Compare commits
79 Commits
204-animat
...
232-fix-ui
Author | SHA1 | Date | |
---|---|---|---|
bb342951a5 | |||
438a79177b | |||
08fc49c40f | |||
d7e25b1192 | |||
1671422143 | |||
a9620c0752 | |||
583beaf6fe | |||
126d2338f2 | |||
24b0c0ed8a | |||
07fc1cf000 | |||
8e0131b3a8 | |||
912a2d524c | |||
82a5124d64 | |||
cca5bf903b | |||
4c0d4cb1c7 | |||
ca64797641 | |||
a49d9ee437 | |||
4c1cd6b8f8 | |||
d6cda709a6 | |||
4bc0feb3a5 | |||
59aab7159a | |||
0a798bb21b | |||
beaafce4fa | |||
6f77337832 | |||
07252d62df | |||
ac615cd134 | |||
a4f9f6e051 | |||
10de88cdc9 | |||
e4fa34c984 | |||
0dd685d009 | |||
3e8c30c0ab | |||
21cc3cd917 | |||
81e620ee3d | |||
fb9bd108bd | |||
4846fbb589 | |||
cab6625399 | |||
1b81251a11 | |||
cf44e547a2 | |||
0269363c63 | |||
4ffca17c54 | |||
77bf269fb5 | |||
1e675132d4 | |||
781287c390 | |||
fc91b252f4 | |||
41c7c569ca | |||
a82d263f05 | |||
d4476baa1b | |||
467e98906e | |||
64a25f94a3 | |||
e6996316be | |||
cf4cd94096 | |||
e9ab633aac | |||
297639e80f | |||
f0cc63b6f0 | |||
d433e8f241 | |||
963a551a1e | |||
d01a434fb7 | |||
3a5a5fc633 | |||
8926ab44e1 | |||
a8f96581c5 | |||
440a4cc1cd | |||
a207c51973 | |||
e706c8e0bf | |||
e590e896da | |||
4140b62a8e | |||
6d9c9c4532 | |||
e9e1911840 | |||
ab350ca7bc | |||
0a8f0b5f88 | |||
2238a15c11 | |||
de73f022e6 | |||
f517205647 | |||
f33c3d8481 | |||
71c69000ec | |||
de1e684b3c | |||
d87730cffb | |||
3f005b86ab | |||
5098e5135d | |||
37cfed489c |
32
CHANGELOG.md
@ -186,4 +186,34 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Exception access violation during Undo/Redo
|
- Exception access violation during Undo/Redo
|
||||||
- Sync missing armature bone Roll
|
- Sync missing armature bone Roll
|
||||||
- Sync missing driver data_path
|
- Sync missing driver data_path
|
||||||
- Constraint replication
|
- Constraint replication
|
||||||
|
|
||||||
|
## [0.4.0] - 2021-07-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Connection preset system (@Kysios)
|
||||||
|
- Display connected users active mode (users pannel and viewport) (@Kysios)
|
||||||
|
- Delta-based replication
|
||||||
|
- Sync timeline marker
|
||||||
|
- Sync images settings (@Kysios)
|
||||||
|
- Sync parent relation type (@Kysios)
|
||||||
|
- Sync uv project modifier
|
||||||
|
- Sync FCurves modifiers
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- User selection optimizations (draw and sync) (@Kysios)
|
||||||
|
- Improved shapekey syncing performances
|
||||||
|
- Improved gpencil syncing performances
|
||||||
|
- Integrate replication as a submodule
|
||||||
|
- The dependencies are now installed in a folder(blender addon folder) that no longer requires administrative rights
|
||||||
|
- Presence overlay UI optimization (@Kysios)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- User selection bounding box glitches for non-mesh objects (@Kysios)
|
||||||
|
- Transforms replication for animated objects
|
||||||
|
- GPencil fill stroke
|
||||||
|
- Sculpt and GPencil brushes deleted when joining a session (@Kysios)
|
||||||
|
- Auto-updater doesn't work for master and develop builds
|
||||||
|
63
README.md
@ -11,9 +11,8 @@ This tool aims to allow multiple users to work on the same scene over the networ
|
|||||||
|
|
||||||
## Quick installation
|
## Quick installation
|
||||||
|
|
||||||
1. Download latest release [multi_user.zip](https://gitlab.com/slumber/multi-user/-/jobs/artifacts/master/download?job=build).
|
1. Download [latest build](https://gitlab.com/slumber/multi-user/-/jobs/artifacts/develop/download?job=build) or [stable build](https://gitlab.com/slumber/multi-user/-/jobs/artifacts/master/download?job=build).
|
||||||
2. Run blender as administrator (dependencies installation).
|
2. Install last_version.zip from your addon preferences.
|
||||||
3. Install last_version.zip from your addon preferences.
|
|
||||||
|
|
||||||
[Dependencies](#dependencies) will be automatically added to your blender python during installation.
|
[Dependencies](#dependencies) will be automatically added to your blender python during installation.
|
||||||
|
|
||||||
@ -29,35 +28,35 @@ See the [troubleshooting guide](https://slumber.gitlab.io/multi-user/getting_sta
|
|||||||
|
|
||||||
Currently, not all data-block are supported for replication over the wire. The following list summarizes the status for each ones.
|
Currently, not all data-block are supported for replication over the wire. The following list summarizes the status for each ones.
|
||||||
|
|
||||||
| Name | Status | Comment |
|
| Name | Status | Comment |
|
||||||
| -------------- | :----: | :----------------------------------------------------------: |
|
| -------------- | :----: | :---------------------------------------------------------------------: |
|
||||||
| action | ✔️ | |
|
| action | ✔️ | |
|
||||||
| camera | ✔️ | |
|
| camera | ✔️ | |
|
||||||
| collection | ✔️ | |
|
| collection | ✔️ | |
|
||||||
| gpencil | ✔️ | |
|
| gpencil | ✔️ | |
|
||||||
| image | ✔️ | |
|
| image | ✔️ | |
|
||||||
| mesh | ✔️ | |
|
| mesh | ✔️ | |
|
||||||
| material | ✔️ | |
|
| material | ✔️ | |
|
||||||
| node_groups | ✔️ | Material & Geometry only |
|
| node_groups | ✔️ | Material & Geometry only |
|
||||||
| geometry nodes | ✔️ | |
|
| geometry nodes | ✔️ | |
|
||||||
| metaball | ✔️ | |
|
| metaball | ✔️ | |
|
||||||
| object | ✔️ | |
|
| object | ✔️ | |
|
||||||
| texts | ✔️ | |
|
| texts | ✔️ | |
|
||||||
| scene | ✔️ | |
|
| scene | ✔️ | |
|
||||||
| world | ✔️ | |
|
| world | ✔️ | |
|
||||||
| volumes | ✔️ | |
|
| volumes | ✔️ | |
|
||||||
| lightprobes | ✔️ | |
|
| lightprobes | ✔️ | |
|
||||||
| physics | ✔️ | |
|
| physics | ✔️ | |
|
||||||
| curve | ❗ | Nurbs surfaces not supported |
|
| textures | ✔️ | |
|
||||||
| textures | ❗ | Supported for modifiers/materials/geo nodes only |
|
| curve | ❗ | Nurbs surfaces not supported |
|
||||||
| armature | ❗ | Not stable |
|
| armature | ❗ | Only for Mesh. [Planned for GPencil](https://gitlab.com/slumber/multi-user/-/issues/161). Not stable yet |
|
||||||
| particles | ❗ | The cache isn't syncing. |
|
| particles | ❗ | The cache isn't syncing. |
|
||||||
| speakers | ❗ | [Partial](https://gitlab.com/slumber/multi-user/-/issues/65) |
|
| speakers | ❗ | [Partial](https://gitlab.com/slumber/multi-user/-/issues/65) |
|
||||||
| vse | ❗ | Mask and Clip not supported yet |
|
| vse | ❗ | Mask and Clip not supported yet |
|
||||||
| libraries | ❗ | Partial |
|
| libraries | ❌ | |
|
||||||
| nla | ❌ | |
|
| nla | ❌ | |
|
||||||
| texts | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/81) |
|
| texts | ❌ | [Planned for v0.5.0](https://gitlab.com/slumber/multi-user/-/issues/81) |
|
||||||
| compositing | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/46) |
|
| compositing | ❌ | [Planned for v0.5.0](https://gitlab.com/slumber/multi-user/-/issues/46) |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,10 +19,10 @@ import sys
|
|||||||
|
|
||||||
project = 'multi-user'
|
project = 'multi-user'
|
||||||
copyright = '2020, Swann Martinez'
|
copyright = '2020, Swann Martinez'
|
||||||
author = 'Swann Martinez, with contributions from Poochy'
|
author = 'Swann Martinez, Poochy, Fabian'
|
||||||
|
|
||||||
# The full version, including alpha/beta/rc tags
|
# The full version, including alpha/beta/rc tags
|
||||||
release = '0.2.0'
|
release = '0.5.0-develop'
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 365 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 26 KiB |
@ -215,8 +215,10 @@ One of the most vital tools is the **Online user panel**. It lists all connected
|
|||||||
users' information including your own:
|
users' information including your own:
|
||||||
|
|
||||||
* **Role** : if a user is an admin or a regular user.
|
* **Role** : if a user is an admin or a regular user.
|
||||||
* **Location**: Where the user is actually working.
|
* **Username** : Name of the user.
|
||||||
|
* **Mode** : User's active editing mode (edit_mesh, paint,etc.).
|
||||||
* **Frame**: When (on which frame) the user is working.
|
* **Frame**: When (on which frame) the user is working.
|
||||||
|
* **Location**: Where the user is actually working.
|
||||||
* **Ping**: user's connection delay in milliseconds
|
* **Ping**: user's connection delay in milliseconds
|
||||||
|
|
||||||
.. figure:: img/quickstart_users.png
|
.. figure:: img/quickstart_users.png
|
||||||
@ -273,6 +275,7 @@ it draw users' related information in your viewport such as:
|
|||||||
|
|
||||||
* Username
|
* Username
|
||||||
* User point of view
|
* User point of view
|
||||||
|
* User active mode
|
||||||
* User selection
|
* User selection
|
||||||
|
|
||||||
.. figure:: img/quickstart_presence.png
|
.. figure:: img/quickstart_presence.png
|
||||||
|
@ -212,14 +212,14 @@ You can run the dedicated server on any platform by following these steps:
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
replication.server
|
replication.serve
|
||||||
|
|
||||||
.. hint::
|
.. hint::
|
||||||
You can also specify a custom **port** (-p), **timeout** (-t), **admin password** (-pwd), **log level (ERROR, WARNING, INFO or DEBUG)** (-l) and **log file** (-lf) with the following optional arguments
|
You can also specify a custom **port** (-p), **timeout** (-t), **admin password** (-pwd), **log level (ERROR, WARNING, INFO or DEBUG)** (-l) and **log file** (-lf) with the following optional arguments
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
replication.server -p 5555 -pwd admin -t 5000 -l INFO -lf server.log
|
replication.serve -p 5555 -pwd admin -t 5000 -l INFO -lf server.log
|
||||||
|
|
||||||
Here, for example, a server is instantiated on port 5555, with password 'admin', a 5 second timeout, and logging enabled.
|
Here, for example, a server is instantiated on port 5555, with password 'admin', a 5 second timeout, and logging enabled.
|
||||||
|
|
||||||
@ -562,7 +562,7 @@ The default Docker image essentially runs the equivalent of:
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
replication.server -pwd admin -p 5555 -t 5000 -l DEBUG -lf multiuser_server.log
|
replication.serve -pwd admin -p 5555 -t 5000 -l DEBUG -lf multiuser_server.log
|
||||||
|
|
||||||
This means the server will be launched with 'admin' as the administrator password, run on ports 5555:5558, use a timeout of 5 seconds, verbose 'DEBUG' log level, and with log files written to 'multiuser_server.log'. See :ref:`cmd-line` for a description of optional parameters.
|
This means the server will be launched with 'admin' as the administrator password, run on ports 5555:5558, use a timeout of 5 seconds, verbose 'DEBUG' log level, and with log files written to 'multiuser_server.log'. See :ref:`cmd-line` for a description of optional parameters.
|
||||||
|
|
||||||
@ -572,7 +572,7 @@ For example, I would like to launch my server with a different administrator pas
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
python3 -m replication.server -pwd supersecretpassword -p 5555 -t 3000 -l DEBUG -lf logname.log
|
replication.serve -pwd supersecretpassword -p 5555 -t 3000 -l DEBUG -lf logname.log
|
||||||
|
|
||||||
Now, my configuration should look like this:
|
Now, my configuration should look like this:
|
||||||
|
|
||||||
@ -691,7 +691,7 @@ We're finally ready to launch the server. Simply run:
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
python3 -m replication.server -p 5555 -pwd admin -t 5000 -l INFO -lf server.log
|
replication.serve -p 5555 -pwd admin -t 5000 -l INFO -lf server.log
|
||||||
|
|
||||||
See :ref:`cmd-line` for a description of optional parameters
|
See :ref:`cmd-line` for a description of optional parameters
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Multi-User",
|
"name": "Multi-User",
|
||||||
"author": "Swann Martinez",
|
"author": "Swann Martinez",
|
||||||
"version": (0, 5, 0),
|
"version": (0, 4, 0),
|
||||||
"description": "Enable real-time collaborative workflow inside blender",
|
"description": "Enable real-time collaborative workflow inside blender",
|
||||||
"blender": (2, 82, 0),
|
"blender": (2, 82, 0),
|
||||||
"location": "3D View > Sidebar > Multi-User tab",
|
"location": "3D View > Sidebar > Multi-User tab",
|
||||||
@ -61,6 +61,7 @@ def register():
|
|||||||
from . import operators
|
from . import operators
|
||||||
from . import handlers
|
from . import handlers
|
||||||
from . import ui
|
from . import ui
|
||||||
|
from . import icons
|
||||||
from . import preferences
|
from . import preferences
|
||||||
from . import addon_updater_ops
|
from . import addon_updater_ops
|
||||||
|
|
||||||
@ -70,6 +71,7 @@ def register():
|
|||||||
operators.register()
|
operators.register()
|
||||||
handlers.register()
|
handlers.register()
|
||||||
ui.register()
|
ui.register()
|
||||||
|
icons.register()
|
||||||
except ModuleNotFoundError as e:
|
except ModuleNotFoundError as e:
|
||||||
raise Exception(module_error_msg)
|
raise Exception(module_error_msg)
|
||||||
logging.error(module_error_msg)
|
logging.error(module_error_msg)
|
||||||
@ -83,7 +85,9 @@ def register():
|
|||||||
type=preferences.SessionUser
|
type=preferences.SessionUser
|
||||||
)
|
)
|
||||||
bpy.types.WindowManager.user_index = bpy.props.IntProperty()
|
bpy.types.WindowManager.user_index = bpy.props.IntProperty()
|
||||||
|
bpy.types.WindowManager.server_index = bpy.props.IntProperty()
|
||||||
bpy.types.TOPBAR_MT_file_import.append(operators.menu_func_import)
|
bpy.types.TOPBAR_MT_file_import.append(operators.menu_func_import)
|
||||||
|
bpy.types.TOPBAR_MT_file_export.append(operators.menu_func_export)
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
@ -91,14 +95,17 @@ def unregister():
|
|||||||
from . import operators
|
from . import operators
|
||||||
from . import handlers
|
from . import handlers
|
||||||
from . import ui
|
from . import ui
|
||||||
|
from . import icons
|
||||||
from . import preferences
|
from . import preferences
|
||||||
from . import addon_updater_ops
|
from . import addon_updater_ops
|
||||||
|
|
||||||
bpy.types.TOPBAR_MT_file_import.remove(operators.menu_func_import)
|
bpy.types.TOPBAR_MT_file_import.remove(operators.menu_func_import)
|
||||||
|
bpy.types.TOPBAR_MT_file_export.remove(operators.menu_func_export)
|
||||||
|
|
||||||
presence.unregister()
|
presence.unregister()
|
||||||
addon_updater_ops.unregister()
|
addon_updater_ops.unregister()
|
||||||
ui.unregister()
|
ui.unregister()
|
||||||
|
icons.unregister()
|
||||||
handlers.unregister()
|
handlers.unregister()
|
||||||
operators.unregister()
|
operators.unregister()
|
||||||
preferences.unregister()
|
preferences.unregister()
|
||||||
@ -107,5 +114,6 @@ def unregister():
|
|||||||
del bpy.types.ID.uuid
|
del bpy.types.ID.uuid
|
||||||
del bpy.types.WindowManager.online_users
|
del bpy.types.WindowManager.online_users
|
||||||
del bpy.types.WindowManager.user_index
|
del bpy.types.WindowManager.user_index
|
||||||
|
del bpy.types.WindowManager.server_index
|
||||||
|
|
||||||
environment.unregister()
|
environment.unregister()
|
||||||
|
@ -56,7 +56,7 @@ class BlCamera(ReplicatedDatablock):
|
|||||||
background_images = data.get('background_images')
|
background_images = data.get('background_images')
|
||||||
|
|
||||||
datablock.background_images.clear()
|
datablock.background_images.clear()
|
||||||
|
# TODO: Use image uuid
|
||||||
if background_images:
|
if background_images:
|
||||||
for img_name, img_data in background_images.items():
|
for img_name, img_data in background_images.items():
|
||||||
img_id = img_data.get('image')
|
img_id = img_data.get('image')
|
||||||
|
@ -28,7 +28,8 @@ from replication.protocol import ReplicatedDatablock
|
|||||||
from .bl_datablock import resolve_datablock_from_uuid
|
from .bl_datablock import resolve_datablock_from_uuid
|
||||||
from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies
|
from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies
|
||||||
from ..utils import get_preferences
|
from ..utils import get_preferences
|
||||||
|
from ..timers import is_annotating
|
||||||
|
from .bl_material import load_materials_slots, dump_materials_slots
|
||||||
|
|
||||||
STROKE_POINT = [
|
STROKE_POINT = [
|
||||||
'co',
|
'co',
|
||||||
@ -65,36 +66,9 @@ def dump_stroke(stroke):
|
|||||||
|
|
||||||
:param stroke: target grease pencil stroke
|
:param stroke: target grease pencil stroke
|
||||||
:type stroke: bpy.types.GPencilStroke
|
:type stroke: bpy.types.GPencilStroke
|
||||||
:return: dict
|
:return: (p_count, p_data)
|
||||||
"""
|
"""
|
||||||
|
return (len(stroke.points), np_dump_collection(stroke.points, STROKE_POINT))
|
||||||
assert(stroke)
|
|
||||||
|
|
||||||
dumper = Dumper()
|
|
||||||
dumper.include_filter = [
|
|
||||||
"aspect",
|
|
||||||
"display_mode",
|
|
||||||
"draw_cyclic",
|
|
||||||
"end_cap_mode",
|
|
||||||
"hardeness",
|
|
||||||
"line_width",
|
|
||||||
"material_index",
|
|
||||||
"start_cap_mode",
|
|
||||||
"uv_rotation",
|
|
||||||
"uv_scale",
|
|
||||||
"uv_translation",
|
|
||||||
"vertex_color_fill",
|
|
||||||
]
|
|
||||||
dumped_stroke = dumper.dump(stroke)
|
|
||||||
|
|
||||||
# Stoke points
|
|
||||||
p_count = len(stroke.points)
|
|
||||||
dumped_stroke['p_count'] = p_count
|
|
||||||
dumped_stroke['points'] = np_dump_collection(stroke.points, STROKE_POINT)
|
|
||||||
|
|
||||||
# TODO: uv_factor, uv_rotation
|
|
||||||
|
|
||||||
return dumped_stroke
|
|
||||||
|
|
||||||
|
|
||||||
def load_stroke(stroke_data, stroke):
|
def load_stroke(stroke_data, stroke):
|
||||||
@ -107,12 +81,12 @@ def load_stroke(stroke_data, stroke):
|
|||||||
"""
|
"""
|
||||||
assert(stroke and stroke_data)
|
assert(stroke and stroke_data)
|
||||||
|
|
||||||
stroke.points.add(stroke_data["p_count"])
|
stroke.points.add(stroke_data[0])
|
||||||
np_load_collection(stroke_data['points'], stroke.points, STROKE_POINT)
|
np_load_collection(stroke_data[1], stroke.points, STROKE_POINT)
|
||||||
|
|
||||||
# HACK: Temporary fix to trigger a BKE_gpencil_stroke_geometry_update to
|
# HACK: Temporary fix to trigger a BKE_gpencil_stroke_geometry_update to
|
||||||
# fix fill issues
|
# fix fill issues
|
||||||
stroke.uv_scale = stroke_data["uv_scale"]
|
stroke.uv_scale = 1.0
|
||||||
|
|
||||||
|
|
||||||
def dump_frame(frame):
|
def dump_frame(frame):
|
||||||
@ -147,10 +121,12 @@ def load_frame(frame_data, frame):
|
|||||||
|
|
||||||
assert(frame and frame_data)
|
assert(frame and frame_data)
|
||||||
|
|
||||||
|
# Load stroke points
|
||||||
for stroke_data in frame_data['strokes_points']:
|
for stroke_data in frame_data['strokes_points']:
|
||||||
target_stroke = frame.strokes.new()
|
target_stroke = frame.strokes.new()
|
||||||
load_stroke(stroke_data, target_stroke)
|
load_stroke(stroke_data, target_stroke)
|
||||||
|
|
||||||
|
# Load stroke metadata
|
||||||
np_load_collection(frame_data['strokes'], frame.strokes, STROKE)
|
np_load_collection(frame_data['strokes'], frame.strokes, STROKE)
|
||||||
|
|
||||||
|
|
||||||
@ -170,7 +146,6 @@ def dump_layer(layer):
|
|||||||
'opacity',
|
'opacity',
|
||||||
'channel_color',
|
'channel_color',
|
||||||
'color',
|
'color',
|
||||||
# 'thickness', #TODO: enabling only for annotation
|
|
||||||
'tint_color',
|
'tint_color',
|
||||||
'tint_factor',
|
'tint_factor',
|
||||||
'vertex_paint_opacity',
|
'vertex_paint_opacity',
|
||||||
@ -187,7 +162,7 @@ def dump_layer(layer):
|
|||||||
'hide',
|
'hide',
|
||||||
'annotation_hide',
|
'annotation_hide',
|
||||||
'lock',
|
'lock',
|
||||||
# 'lock_frame',
|
'lock_frame',
|
||||||
# 'lock_material',
|
# 'lock_material',
|
||||||
# 'use_mask_layer',
|
# 'use_mask_layer',
|
||||||
'use_lights',
|
'use_lights',
|
||||||
@ -195,12 +170,13 @@ def dump_layer(layer):
|
|||||||
'select',
|
'select',
|
||||||
'show_points',
|
'show_points',
|
||||||
'show_in_front',
|
'show_in_front',
|
||||||
|
# 'thickness'
|
||||||
# 'parent',
|
# 'parent',
|
||||||
# 'parent_type',
|
# 'parent_type',
|
||||||
# 'parent_bone',
|
# 'parent_bone',
|
||||||
# 'matrix_inverse',
|
# 'matrix_inverse',
|
||||||
]
|
]
|
||||||
if layer.id_data.is_annotation:
|
if layer.thickness != 0:
|
||||||
dumper.include_filter.append('thickness')
|
dumper.include_filter.append('thickness')
|
||||||
|
|
||||||
dumped_layer = dumper.dump(layer)
|
dumped_layer = dumper.dump(layer)
|
||||||
@ -255,10 +231,10 @@ class BlGpencil(ReplicatedDatablock):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load(data: dict, datablock: object):
|
def load(data: dict, datablock: object):
|
||||||
datablock.materials.clear()
|
# MATERIAL SLOTS
|
||||||
if "materials" in data.keys():
|
src_materials = data.get('materials', None)
|
||||||
for mat in data['materials']:
|
if src_materials:
|
||||||
datablock.materials.append(bpy.data.materials[mat])
|
load_materials_slots(src_materials, datablock.materials)
|
||||||
|
|
||||||
loader = Loader()
|
loader = Loader()
|
||||||
loader.load(datablock, data)
|
loader.load(datablock, data)
|
||||||
@ -286,7 +262,6 @@ class BlGpencil(ReplicatedDatablock):
|
|||||||
dumper = Dumper()
|
dumper = Dumper()
|
||||||
dumper.depth = 2
|
dumper.depth = 2
|
||||||
dumper.include_filter = [
|
dumper.include_filter = [
|
||||||
'materials',
|
|
||||||
'name',
|
'name',
|
||||||
'zdepth_offset',
|
'zdepth_offset',
|
||||||
'stroke_thickness_space',
|
'stroke_thickness_space',
|
||||||
@ -294,7 +269,7 @@ class BlGpencil(ReplicatedDatablock):
|
|||||||
'stroke_depth_order'
|
'stroke_depth_order'
|
||||||
]
|
]
|
||||||
data = dumper.dump(datablock)
|
data = dumper.dump(datablock)
|
||||||
|
data['materials'] = dump_materials_slots(datablock.materials)
|
||||||
data['layers'] = {}
|
data['layers'] = {}
|
||||||
|
|
||||||
for layer in datablock.layers:
|
for layer in datablock.layers:
|
||||||
@ -323,7 +298,8 @@ class BlGpencil(ReplicatedDatablock):
|
|||||||
return bpy.context.mode == 'OBJECT' \
|
return bpy.context.mode == 'OBJECT' \
|
||||||
or layer_changed(datablock, data) \
|
or layer_changed(datablock, data) \
|
||||||
or frame_changed(data) \
|
or frame_changed(data) \
|
||||||
or get_preferences().sync_flags.sync_during_editmode
|
or get_preferences().sync_flags.sync_during_editmode \
|
||||||
|
or is_annotating(bpy.context)
|
||||||
|
|
||||||
_type = bpy.types.GreasePencil
|
_type = bpy.types.GreasePencil
|
||||||
_class = BlGpencil
|
_class = BlGpencil
|
||||||
|
@ -69,11 +69,12 @@ class BlImage(ReplicatedDatablock):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def load(data: dict, datablock: object):
|
def load(data: dict, datablock: object):
|
||||||
loader = Loader()
|
loader = Loader()
|
||||||
loader.load(data, datablock)
|
loader.load(datablock, data)
|
||||||
|
|
||||||
|
# datablock.name = data.get('name')
|
||||||
datablock.source = 'FILE'
|
datablock.source = 'FILE'
|
||||||
datablock.filepath_raw = get_filepath(data['filename'])
|
datablock.filepath_raw = get_filepath(data['filename'])
|
||||||
color_space_name = data["colorspace_settings"]["name"]
|
color_space_name = data.get("colorspace")
|
||||||
|
|
||||||
if color_space_name:
|
if color_space_name:
|
||||||
datablock.colorspace_settings.name = color_space_name
|
datablock.colorspace_settings.name = color_space_name
|
||||||
@ -92,12 +93,10 @@ class BlImage(ReplicatedDatablock):
|
|||||||
"name",
|
"name",
|
||||||
# 'source',
|
# 'source',
|
||||||
'size',
|
'size',
|
||||||
'height',
|
'alpha_mode']
|
||||||
'alpha',
|
|
||||||
'float_buffer',
|
|
||||||
'alpha_mode',
|
|
||||||
'colorspace_settings']
|
|
||||||
data.update(dumper.dump(datablock))
|
data.update(dumper.dump(datablock))
|
||||||
|
data['colorspace'] = datablock.colorspace_settings.name
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -132,10 +131,7 @@ class BlImage(ReplicatedDatablock):
|
|||||||
if datablock.is_dirty:
|
if datablock.is_dirty:
|
||||||
datablock.save()
|
datablock.save()
|
||||||
|
|
||||||
if not data or (datablock and (datablock.name != data.get('name'))):
|
return True
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
_type = bpy.types.Image
|
_type = bpy.types.Image
|
||||||
_class = BlImage
|
_class = BlImage
|
||||||
|
@ -124,8 +124,7 @@ def dump_node(node: bpy.types.ShaderNode) -> dict:
|
|||||||
"show_preview",
|
"show_preview",
|
||||||
"show_texture",
|
"show_texture",
|
||||||
"outputs",
|
"outputs",
|
||||||
"width_hidden",
|
"width_hidden"
|
||||||
"image"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
dumped_node = node_dumper.dump(node)
|
dumped_node = node_dumper.dump(node)
|
||||||
@ -388,11 +387,10 @@ def load_materials_slots(src_materials: list, dst_materials: bpy.types.bpy_prop_
|
|||||||
|
|
||||||
for mat_uuid, mat_name in src_materials:
|
for mat_uuid, mat_name in src_materials:
|
||||||
mat_ref = None
|
mat_ref = None
|
||||||
if mat_uuid is not None:
|
if mat_uuid:
|
||||||
mat_ref = get_datablock_from_uuid(mat_uuid, None)
|
mat_ref = get_datablock_from_uuid(mat_uuid, None)
|
||||||
else:
|
else:
|
||||||
mat_ref = bpy.data.materials[mat_name]
|
mat_ref = bpy.data.materials[mat_name]
|
||||||
|
|
||||||
dst_materials.append(mat_ref)
|
dst_materials.append(mat_ref)
|
||||||
|
|
||||||
|
|
||||||
|
@ -620,10 +620,8 @@ class BlObject(ReplicatedDatablock):
|
|||||||
|
|
||||||
transform = data.get('transforms', None)
|
transform = data.get('transforms', None)
|
||||||
if transform:
|
if transform:
|
||||||
datablock.matrix_parent_inverse = mathutils.Matrix(
|
datablock.matrix_parent_inverse = mathutils.Matrix(transform['matrix_parent_inverse'])
|
||||||
transform['matrix_parent_inverse'])
|
|
||||||
datablock.matrix_basis = mathutils.Matrix(transform['matrix_basis'])
|
datablock.matrix_basis = mathutils.Matrix(transform['matrix_basis'])
|
||||||
datablock.matrix_local = mathutils.Matrix(transform['matrix_local'])
|
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -403,8 +403,9 @@ class BlScene(ReplicatedDatablock):
|
|||||||
datablock.world = bpy.data.worlds[data['world']]
|
datablock.world = bpy.data.worlds[data['world']]
|
||||||
|
|
||||||
# Annotation
|
# Annotation
|
||||||
if 'grease_pencil' in data.keys():
|
gpencil_uid = data.get('grease_pencil')
|
||||||
datablock.grease_pencil = bpy.data.grease_pencils[data['grease_pencil']]
|
if gpencil_uid:
|
||||||
|
datablock.grease_pencil = resolve_datablock_from_uuid(gpencil_uid, bpy.data.grease_pencils)
|
||||||
|
|
||||||
if get_preferences().sync_flags.sync_render_settings:
|
if get_preferences().sync_flags.sync_render_settings:
|
||||||
if 'eevee' in data.keys():
|
if 'eevee' in data.keys():
|
||||||
@ -445,6 +446,15 @@ class BlScene(ReplicatedDatablock):
|
|||||||
elif datablock.sequence_editor and not sequences:
|
elif datablock.sequence_editor and not sequences:
|
||||||
datablock.sequence_editor_clear()
|
datablock.sequence_editor_clear()
|
||||||
|
|
||||||
|
# Timeline markers
|
||||||
|
markers = data.get('timeline_markers')
|
||||||
|
if markers:
|
||||||
|
datablock.timeline_markers.clear()
|
||||||
|
for name, frame, camera in markers:
|
||||||
|
marker = datablock.timeline_markers.new(name, frame=frame)
|
||||||
|
if camera:
|
||||||
|
marker.camera = resolve_datablock_from_uuid(camera, bpy.data.objects)
|
||||||
|
marker.select = False
|
||||||
# FIXME: Find a better way after the replication big refacotoring
|
# FIXME: Find a better way after the replication big refacotoring
|
||||||
# Keep other user from deleting collection object by flushing their history
|
# Keep other user from deleting collection object by flushing their history
|
||||||
flush_history()
|
flush_history()
|
||||||
@ -461,7 +471,6 @@ class BlScene(ReplicatedDatablock):
|
|||||||
'name',
|
'name',
|
||||||
'world',
|
'world',
|
||||||
'id',
|
'id',
|
||||||
'grease_pencil',
|
|
||||||
'frame_start',
|
'frame_start',
|
||||||
'frame_end',
|
'frame_end',
|
||||||
'frame_step',
|
'frame_step',
|
||||||
@ -517,6 +526,13 @@ class BlScene(ReplicatedDatablock):
|
|||||||
dumped_sequences[seq.name] = dump_sequence(seq)
|
dumped_sequences[seq.name] = dump_sequence(seq)
|
||||||
data['sequences'] = dumped_sequences
|
data['sequences'] = dumped_sequences
|
||||||
|
|
||||||
|
# Timeline markers
|
||||||
|
if datablock.timeline_markers:
|
||||||
|
data['timeline_markers'] = [(m.name, m.frame, getattr(m.camera, 'uuid', None)) for m in datablock.timeline_markers]
|
||||||
|
|
||||||
|
if datablock.grease_pencil:
|
||||||
|
data['grease_pencil'] = datablock.grease_pencil.uuid
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -52,7 +52,8 @@ def sanitize_deps_graph(remove_nodes: bool = False):
|
|||||||
def update_external_dependencies():
|
def update_external_dependencies():
|
||||||
"""Force external dependencies(files such as images) evaluation
|
"""Force external dependencies(files such as images) evaluation
|
||||||
"""
|
"""
|
||||||
nodes_ids = [n.uuid for n in session.repository.graph.values() if n.data['type_id'] in ['WindowsPath', 'PosixPath']]
|
external_types = ['WindowsPath', 'PosixPath', 'Image']
|
||||||
|
nodes_ids = [n.uuid for n in session.repository.graph.values() if n.data['type_id'] in external_types]
|
||||||
for node_id in nodes_ids:
|
for node_id in nodes_ids:
|
||||||
node = session.repository.graph.get(node_id)
|
node = session.repository.graph.get(node_id)
|
||||||
if node and node.owner in [session.repository.username, RP_COMMON]:
|
if node and node.owner in [session.repository.username, RP_COMMON]:
|
||||||
@ -78,8 +79,6 @@ def on_scene_update(scene):
|
|||||||
logging.debug(f"Ignoring distant update of {dependency_updates[0].id.name}")
|
logging.debug(f"Ignoring distant update of {dependency_updates[0].id.name}")
|
||||||
return
|
return
|
||||||
|
|
||||||
update_external_dependencies()
|
|
||||||
|
|
||||||
# NOTE: maybe we don't need to check each update but only the first
|
# NOTE: maybe we don't need to check each update but only the first
|
||||||
for update in reversed(dependency_updates):
|
for update in reversed(dependency_updates):
|
||||||
update_uuid = getattr(update.id, 'uuid', None)
|
update_uuid = getattr(update.id, 'uuid', None)
|
||||||
@ -103,10 +102,16 @@ def on_scene_update(scene):
|
|||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
elif isinstance(update.id, bpy.types.Scene):
|
elif isinstance(update.id, bpy.types.Scene):
|
||||||
scn_uuid = porcelain.add(session.repository, update.id)
|
scene = bpy.data.scenes.get(update.id.name)
|
||||||
|
scn_uuid = porcelain.add(session.repository, scene)
|
||||||
porcelain.commit(session.repository, scn_uuid)
|
porcelain.commit(session.repository, scn_uuid)
|
||||||
porcelain.push(session.repository, 'origin', scn_uuid)
|
porcelain.push(session.repository, 'origin', scn_uuid)
|
||||||
|
|
||||||
|
scene_graph_changed = [u for u in reversed(dependency_updates) if getattr(u.id, 'uuid', None) and isinstance(u.id,(bpy.types.Scene,bpy.types.Collection))]
|
||||||
|
if scene_graph_changed:
|
||||||
|
porcelain.purge_orphan_nodes(session.repository)
|
||||||
|
|
||||||
|
update_external_dependencies()
|
||||||
|
|
||||||
@persistent
|
@persistent
|
||||||
def resolve_deps_graph(dummy):
|
def resolve_deps_graph(dummy):
|
||||||
|
45
multi_user/icons/__init__.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# ##### 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 bpy
|
||||||
|
import os
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import bpy.utils.previews
|
||||||
|
|
||||||
|
def register():
|
||||||
|
|
||||||
|
global icons_col
|
||||||
|
|
||||||
|
pcoll = bpy.utils.previews.new()
|
||||||
|
icons_dir = os.path.join(os.path.dirname(__file__), ".")
|
||||||
|
for png in Path(icons_dir).rglob("*.png"):
|
||||||
|
pcoll.load(png.stem, str(png), "IMAGE")
|
||||||
|
|
||||||
|
icons_col = pcoll
|
||||||
|
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
|
||||||
|
global icons_col
|
||||||
|
|
||||||
|
try:
|
||||||
|
bpy.utils.previews.remove(icons_col)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
icons_col = None
|
BIN
multi_user/icons/repository_merge.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
multi_user/icons/repository_push.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
multi_user/icons/server_offline.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
multi_user/icons/server_online.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
multi_user/icons/session_status_offline.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
multi_user/icons/session_status_online.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
multi_user/icons/session_status_waiting.png
Normal file
After Width: | Height: | Size: 11 KiB |
@ -20,6 +20,7 @@ import asyncio
|
|||||||
import copy
|
import copy
|
||||||
import gzip
|
import gzip
|
||||||
import logging
|
import logging
|
||||||
|
from multi_user.preferences import ServerPreset
|
||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
import random
|
import random
|
||||||
@ -28,6 +29,7 @@ import string
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
from uuid import uuid4
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -56,7 +58,7 @@ from replication.repository import Repository
|
|||||||
|
|
||||||
from . import bl_types, environment, shared_data, timers, ui, utils
|
from . import bl_types, environment, shared_data, timers, ui, utils
|
||||||
from .handlers import on_scene_update, sanitize_deps_graph
|
from .handlers import on_scene_update, sanitize_deps_graph
|
||||||
from .presence import SessionStatusWidget, renderer, view3d_find
|
from .presence import SessionStatusWidget, renderer, view3d_find, refresh_sidebar_view
|
||||||
from .timers import registry
|
from .timers import registry
|
||||||
|
|
||||||
background_execution_queue = Queue()
|
background_execution_queue = Queue()
|
||||||
@ -81,7 +83,6 @@ def session_callback(name):
|
|||||||
def initialize_session():
|
def initialize_session():
|
||||||
"""Session connection init hander
|
"""Session connection init hander
|
||||||
"""
|
"""
|
||||||
settings = utils.get_preferences()
|
|
||||||
runtime_settings = bpy.context.window_manager.session
|
runtime_settings = bpy.context.window_manager.session
|
||||||
|
|
||||||
if not runtime_settings.is_host:
|
if not runtime_settings.is_host:
|
||||||
@ -108,7 +109,6 @@ def initialize_session():
|
|||||||
for d in deleyables:
|
for d in deleyables:
|
||||||
d.register()
|
d.register()
|
||||||
|
|
||||||
|
|
||||||
# Step 5: Clearing history
|
# Step 5: Clearing history
|
||||||
utils.flush_history()
|
utils.flush_history()
|
||||||
|
|
||||||
@ -142,16 +142,143 @@ def on_connection_end(reason="none"):
|
|||||||
if isinstance(handler, logging.FileHandler):
|
if isinstance(handler, logging.FileHandler):
|
||||||
logger.removeHandler(handler)
|
logger.removeHandler(handler)
|
||||||
if reason != "user":
|
if reason != "user":
|
||||||
bpy.ops.session.notify('INVOKE_DEFAULT', message=f"Disconnected from session. Reason: {reason}. ")
|
bpy.ops.session.notify('INVOKE_DEFAULT', message=f"Disconnected from session. Reason: {reason}. ") #TODO: change op session.notify to add ui + change reason (in replication->interface)
|
||||||
|
|
||||||
|
def setup_logging():
|
||||||
|
""" Session setup logging (host/connect)
|
||||||
|
"""
|
||||||
|
settings = utils.get_preferences()
|
||||||
|
logger = logging.getLogger()
|
||||||
|
if len(logger.handlers) == 1:
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
fmt='%(asctime)s CLIENT %(levelname)-8s %(message)s',
|
||||||
|
datefmt='%H:%M:%S'
|
||||||
|
)
|
||||||
|
|
||||||
|
start_time = datetime.now().strftime('%Y_%m_%d_%H-%M-%S')
|
||||||
|
log_directory = os.path.join(
|
||||||
|
settings.cache_directory,
|
||||||
|
f"multiuser_{start_time}.log")
|
||||||
|
|
||||||
|
os.makedirs(settings.cache_directory, exist_ok=True)
|
||||||
|
|
||||||
|
handler = logging.FileHandler(log_directory, mode='w')
|
||||||
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
for handler in logger.handlers:
|
||||||
|
if isinstance(handler, logging.NullHandler):
|
||||||
|
continue
|
||||||
|
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
def setup_timer():
|
||||||
|
""" Session setup timer (host/connect)
|
||||||
|
"""
|
||||||
|
settings = utils.get_preferences()
|
||||||
|
deleyables.append(timers.ClientUpdate())
|
||||||
|
deleyables.append(timers.DynamicRightSelectTimer())
|
||||||
|
deleyables.append(timers.ApplyTimer(timeout=settings.depsgraph_update_rate))
|
||||||
|
|
||||||
|
session_update = timers.SessionStatusUpdate()
|
||||||
|
session_user_sync = timers.SessionUserSync()
|
||||||
|
session_background_executor = timers.MainThreadExecutor(execution_queue=background_execution_queue)
|
||||||
|
session_listen = timers.SessionListenTimer(timeout=0.001)
|
||||||
|
|
||||||
|
session_listen.register()
|
||||||
|
session_update.register()
|
||||||
|
session_user_sync.register()
|
||||||
|
session_background_executor.register()
|
||||||
|
|
||||||
|
deleyables.append(session_background_executor)
|
||||||
|
deleyables.append(session_update)
|
||||||
|
deleyables.append(session_user_sync)
|
||||||
|
deleyables.append(session_listen)
|
||||||
|
deleyables.append(timers.AnnotationUpdates())
|
||||||
|
|
||||||
|
def get_active_server_preset(context):
|
||||||
|
active_index = context.window_manager.server_index
|
||||||
|
server_presets = utils.get_preferences().server_preset
|
||||||
|
|
||||||
|
active_index = active_index if active_index <= len(server_presets)-1 else 0
|
||||||
|
|
||||||
|
return server_presets[active_index]
|
||||||
|
|
||||||
# OPERATORS
|
# OPERATORS
|
||||||
class SessionStartOperator(bpy.types.Operator):
|
class SessionConnectOperator(bpy.types.Operator):
|
||||||
bl_idname = "session.start"
|
bl_idname = "session.connect"
|
||||||
bl_label = "start"
|
bl_label = "connect"
|
||||||
bl_description = "connect to a net server"
|
bl_description = "connect to a net server"
|
||||||
|
|
||||||
host: bpy.props.BoolProperty(default=False)
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
global deleyables
|
||||||
|
|
||||||
|
settings = utils.get_preferences()
|
||||||
|
users = bpy.data.window_managers['WinMan'].online_users
|
||||||
|
active_server = get_active_server_preset(context)
|
||||||
|
admin_pass = active_server.admin_password if active_server.use_admin_password else None
|
||||||
|
server_pass = active_server.server_password if active_server.use_server_password else ''
|
||||||
|
|
||||||
|
users.clear()
|
||||||
|
deleyables.clear()
|
||||||
|
|
||||||
|
setup_logging()
|
||||||
|
|
||||||
|
bpy_protocol = bl_types.get_data_translation_protocol()
|
||||||
|
|
||||||
|
# Check if supported_datablocks are up to date before starting the
|
||||||
|
# the session
|
||||||
|
for dcc_type_id in bpy_protocol.implementations.keys():
|
||||||
|
if dcc_type_id not in settings.supported_datablocks:
|
||||||
|
logging.info(f"{dcc_type_id} not found, \
|
||||||
|
regenerate type settings...")
|
||||||
|
settings.generate_supported_types()
|
||||||
|
|
||||||
|
|
||||||
|
if bpy.app.version[1] >= 91:
|
||||||
|
python_binary_path = sys.executable
|
||||||
|
else:
|
||||||
|
python_binary_path = bpy.app.binary_path_python
|
||||||
|
|
||||||
|
repo = Repository(
|
||||||
|
rdp=bpy_protocol,
|
||||||
|
username=settings.username)
|
||||||
|
|
||||||
|
# Join a session
|
||||||
|
if not active_server.use_admin_password:
|
||||||
|
utils.clean_scene()
|
||||||
|
|
||||||
|
try:
|
||||||
|
porcelain.remote_add(
|
||||||
|
repo,
|
||||||
|
'origin',
|
||||||
|
active_server.ip,
|
||||||
|
active_server.port,
|
||||||
|
server_password=server_pass,
|
||||||
|
admin_password=admin_pass)
|
||||||
|
session.connect(
|
||||||
|
repository= repo,
|
||||||
|
timeout=settings.connection_timeout,
|
||||||
|
server_password=server_pass,
|
||||||
|
admin_password=admin_pass
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.report({'ERROR'}, str(e))
|
||||||
|
logging.error(str(e))
|
||||||
|
|
||||||
|
# Background client updates service
|
||||||
|
setup_timer()
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class SessionHostOperator(bpy.types.Operator):
|
||||||
|
bl_idname = "session.host"
|
||||||
|
bl_label = "host"
|
||||||
|
bl_description = "host server"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
@ -163,33 +290,13 @@ class SessionStartOperator(bpy.types.Operator):
|
|||||||
settings = utils.get_preferences()
|
settings = utils.get_preferences()
|
||||||
runtime_settings = context.window_manager.session
|
runtime_settings = context.window_manager.session
|
||||||
users = bpy.data.window_managers['WinMan'].online_users
|
users = bpy.data.window_managers['WinMan'].online_users
|
||||||
admin_pass = settings.password
|
admin_pass = settings.host_admin_password if settings.host_use_admin_password else None
|
||||||
|
server_pass = settings.host_server_password if settings.host_use_server_password else ''
|
||||||
|
|
||||||
users.clear()
|
users.clear()
|
||||||
deleyables.clear()
|
deleyables.clear()
|
||||||
|
|
||||||
logger = logging.getLogger()
|
setup_logging()
|
||||||
if len(logger.handlers) == 1:
|
|
||||||
formatter = logging.Formatter(
|
|
||||||
fmt='%(asctime)s CLIENT %(levelname)-8s %(message)s',
|
|
||||||
datefmt='%H:%M:%S'
|
|
||||||
)
|
|
||||||
|
|
||||||
start_time = datetime.now().strftime('%Y_%m_%d_%H-%M-%S')
|
|
||||||
log_directory = os.path.join(
|
|
||||||
settings.cache_directory,
|
|
||||||
f"multiuser_{start_time}.log")
|
|
||||||
|
|
||||||
os.makedirs(settings.cache_directory, exist_ok=True)
|
|
||||||
|
|
||||||
handler = logging.FileHandler(log_directory, mode='w')
|
|
||||||
logger.addHandler(handler)
|
|
||||||
|
|
||||||
for handler in logger.handlers:
|
|
||||||
if isinstance(handler, logging.NullHandler):
|
|
||||||
continue
|
|
||||||
|
|
||||||
handler.setFormatter(formatter)
|
|
||||||
|
|
||||||
bpy_protocol = bl_types.get_data_translation_protocol()
|
bpy_protocol = bl_types.get_data_translation_protocol()
|
||||||
|
|
||||||
@ -212,80 +319,38 @@ class SessionStartOperator(bpy.types.Operator):
|
|||||||
username=settings.username)
|
username=settings.username)
|
||||||
|
|
||||||
# Host a session
|
# Host a session
|
||||||
if self.host:
|
if settings.init_method == 'EMPTY':
|
||||||
if settings.init_method == 'EMPTY':
|
utils.clean_scene()
|
||||||
utils.clean_scene()
|
|
||||||
|
|
||||||
runtime_settings.is_host = True
|
try:
|
||||||
runtime_settings.internet_ip = environment.get_ip()
|
# Init repository
|
||||||
|
for scene in bpy.data.scenes:
|
||||||
|
porcelain.add(repo, scene)
|
||||||
|
|
||||||
try:
|
porcelain.remote_add(
|
||||||
# Init repository
|
repo,
|
||||||
for scene in bpy.data.scenes:
|
'origin',
|
||||||
porcelain.add(repo, scene)
|
'127.0.0.1',
|
||||||
|
settings.host_port,
|
||||||
porcelain.remote_add(
|
server_password=server_pass,
|
||||||
repo,
|
admin_password=admin_pass)
|
||||||
'origin',
|
session.host(
|
||||||
'127.0.0.1',
|
repository= repo,
|
||||||
settings.port,
|
remote='origin',
|
||||||
admin_password=admin_pass)
|
timeout=settings.connection_timeout,
|
||||||
session.host(
|
server_password=server_pass,
|
||||||
repository= repo,
|
admin_password=admin_pass,
|
||||||
remote='origin',
|
cache_directory=settings.cache_directory,
|
||||||
timeout=settings.connection_timeout,
|
server_log_level=logging.getLevelName(
|
||||||
password=admin_pass,
|
logging.getLogger().level),
|
||||||
cache_directory=settings.cache_directory,
|
)
|
||||||
server_log_level=logging.getLevelName(
|
except Exception as e:
|
||||||
logging.getLogger().level),
|
self.report({'ERROR'}, repr(e))
|
||||||
)
|
logging.error(f"Error: {e}")
|
||||||
except Exception as e:
|
traceback.print_exc()
|
||||||
self.report({'ERROR'}, repr(e))
|
|
||||||
logging.error(f"Error: {e}")
|
|
||||||
traceback.print_exc()
|
|
||||||
# Join a session
|
|
||||||
else:
|
|
||||||
if not runtime_settings.admin:
|
|
||||||
utils.clean_scene()
|
|
||||||
# regular session, no password needed
|
|
||||||
admin_pass = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
porcelain.remote_add(
|
|
||||||
repo,
|
|
||||||
'origin',
|
|
||||||
settings.ip,
|
|
||||||
settings.port,
|
|
||||||
admin_password=admin_pass)
|
|
||||||
session.connect(
|
|
||||||
repository= repo,
|
|
||||||
timeout=settings.connection_timeout,
|
|
||||||
password=admin_pass
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
self.report({'ERROR'}, str(e))
|
|
||||||
logging.error(str(e))
|
|
||||||
|
|
||||||
# Background client updates service
|
# Background client updates service
|
||||||
deleyables.append(timers.ClientUpdate())
|
setup_timer()
|
||||||
deleyables.append(timers.DynamicRightSelectTimer())
|
|
||||||
deleyables.append(timers.ApplyTimer(timeout=settings.depsgraph_update_rate))
|
|
||||||
|
|
||||||
session_update = timers.SessionStatusUpdate()
|
|
||||||
session_user_sync = timers.SessionUserSync()
|
|
||||||
session_background_executor = timers.MainThreadExecutor(
|
|
||||||
execution_queue=background_execution_queue)
|
|
||||||
session_listen = timers.SessionListenTimer(timeout=0.001)
|
|
||||||
|
|
||||||
session_listen.register()
|
|
||||||
session_update.register()
|
|
||||||
session_user_sync.register()
|
|
||||||
session_background_executor.register()
|
|
||||||
|
|
||||||
deleyables.append(session_background_executor)
|
|
||||||
deleyables.append(session_update)
|
|
||||||
deleyables.append(session_user_sync)
|
|
||||||
deleyables.append(session_listen)
|
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@ -689,7 +754,6 @@ class SessionPurgeOperator(bpy.types.Operator):
|
|||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
try:
|
try:
|
||||||
sanitize_deps_graph(remove_nodes=True)
|
|
||||||
porcelain.purge_orphan_nodes(session.repository)
|
porcelain.purge_orphan_nodes(session.repository)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.report({'ERROR'}, repr(e))
|
self.report({'ERROR'}, repr(e))
|
||||||
@ -784,7 +848,6 @@ class SessionStopAutoSaveOperator(bpy.types.Operator):
|
|||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
|
class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
|
||||||
bl_idname = "session.load"
|
bl_idname = "session.load"
|
||||||
bl_label = "Load session save"
|
bl_label = "Load session save"
|
||||||
@ -832,12 +895,90 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
|
|||||||
class SessionPresetServerAdd(bpy.types.Operator):
|
class SessionPresetServerAdd(bpy.types.Operator):
|
||||||
"""Add a server to the server list preset"""
|
"""Add a server to the server list preset"""
|
||||||
bl_idname = "session.preset_server_add"
|
bl_idname = "session.preset_server_add"
|
||||||
bl_label = "add server preset"
|
bl_label = "Add server preset"
|
||||||
bl_description = "add the current server to the server preset list"
|
bl_description = "add a server to the server preset list"
|
||||||
bl_options = {"REGISTER"}
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
name : bpy.props.StringProperty(default="server_preset")
|
server_name: bpy.props.StringProperty(default="")
|
||||||
|
ip: bpy.props.StringProperty(default="127.0.0.1")
|
||||||
|
port: bpy.props.IntProperty(default=5555)
|
||||||
|
use_server_password: bpy.props.BoolProperty(default=False)
|
||||||
|
server_password: bpy.props.StringProperty(default="", subtype = "PASSWORD")
|
||||||
|
use_admin_password: bpy.props.BoolProperty(default=False)
|
||||||
|
admin_password: bpy.props.StringProperty(default="", subtype = "PASSWORD")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
self.server_name = ""
|
||||||
|
self.ip = "127.0.0.1"
|
||||||
|
self.port = 5555
|
||||||
|
self.use_server_password = False
|
||||||
|
self.server_password = ""
|
||||||
|
self.use_admin_password = False
|
||||||
|
self.admin_password = ""
|
||||||
|
|
||||||
|
assert(context)
|
||||||
|
return context.window_manager.invoke_props_dialog(self)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(self, "server_name", text="Server name")
|
||||||
|
row = layout.row(align = True)
|
||||||
|
row.prop(self, "ip", text="IP+port")
|
||||||
|
row.prop(self, "port", text="")
|
||||||
|
row = layout.row()
|
||||||
|
col = row.column()
|
||||||
|
col.prop(self, "use_server_password", text="Server password:")
|
||||||
|
col = row.column()
|
||||||
|
col.enabled = True if self.use_server_password else False
|
||||||
|
col.prop(self, "server_password", text="")
|
||||||
|
row = layout.row()
|
||||||
|
col = row.column()
|
||||||
|
col.prop(self, "use_admin_password", text="Admin password:")
|
||||||
|
col = row.column()
|
||||||
|
col.enabled = True if self.use_admin_password else False
|
||||||
|
col.prop(self, "admin_password", text="")
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
assert(context)
|
||||||
|
|
||||||
|
settings = utils.get_preferences()
|
||||||
|
existing_preset = settings.get_server_preset(self.server_name)
|
||||||
|
|
||||||
|
new_server = existing_preset if existing_preset else settings.server_preset.add()
|
||||||
|
new_server.name = str(uuid4())
|
||||||
|
new_server.server_name = self.server_name
|
||||||
|
new_server.ip = self.ip
|
||||||
|
new_server.port = self.port
|
||||||
|
new_server.use_server_password = self.use_server_password
|
||||||
|
new_server.server_password = self.server_password
|
||||||
|
new_server.use_admin_password = self.use_admin_password
|
||||||
|
new_server.admin_password = self.admin_password
|
||||||
|
|
||||||
|
refresh_sidebar_view()
|
||||||
|
|
||||||
|
if new_server == existing_preset :
|
||||||
|
self.report({'INFO'}, "Server '" + self.server_name + "' edited")
|
||||||
|
else :
|
||||||
|
self.report({'INFO'}, "New '" + self.server_name + "' server preset")
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class SessionPresetServerEdit(bpy.types.Operator): # TODO : use preset, not settings
|
||||||
|
"""Edit a server to the server list preset"""
|
||||||
|
bl_idname = "session.preset_server_edit"
|
||||||
|
bl_label = "Edit server preset"
|
||||||
|
bl_description = "Edit a server from the server preset list"
|
||||||
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
|
target_server_name: bpy.props.StringProperty(default="None")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return True
|
return True
|
||||||
@ -848,35 +989,39 @@ class SessionPresetServerAdd(bpy.types.Operator):
|
|||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
|
||||||
col = layout.column()
|
|
||||||
settings = utils.get_preferences()
|
settings = utils.get_preferences()
|
||||||
|
settings_active_server = settings.server_preset.get(self.target_server_name)
|
||||||
|
|
||||||
col.prop(settings, "server_name", text="server name")
|
row = layout.row()
|
||||||
|
row.prop(settings_active_server, "server_name", text="Server name")
|
||||||
|
row = layout.row(align = True)
|
||||||
|
row.prop(settings_active_server, "ip", text="IP+port")
|
||||||
|
row.prop(settings_active_server, "port", text="")
|
||||||
|
row = layout.row()
|
||||||
|
col = row.column()
|
||||||
|
col.prop(settings_active_server, "use_server_password", text="Server password:")
|
||||||
|
col = row.column()
|
||||||
|
col.enabled = True if settings_active_server.use_server_password else False
|
||||||
|
col.prop(settings_active_server, "server_password", text="")
|
||||||
|
row = layout.row()
|
||||||
|
col = row.column()
|
||||||
|
col.prop(settings_active_server, "use_admin_password", text="Admin password:")
|
||||||
|
col = row.column()
|
||||||
|
col.enabled = True if settings_active_server.use_admin_password else False
|
||||||
|
col.prop(settings_active_server, "admin_password", text="")
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
assert(context)
|
assert(context)
|
||||||
|
|
||||||
settings = utils.get_preferences()
|
settings = utils.get_preferences()
|
||||||
|
settings_active_server = settings.server_preset.get(self.target_server_name)
|
||||||
|
|
||||||
existing_preset = settings.server_preset.get(settings.server_name)
|
refresh_sidebar_view()
|
||||||
|
|
||||||
new_server = existing_preset if existing_preset else settings.server_preset.add()
|
self.report({'INFO'}, "Server '" + settings_active_server.server_name + "' edited")
|
||||||
new_server.name = settings.server_name
|
|
||||||
new_server.server_ip = settings.ip
|
|
||||||
new_server.server_port = settings.port
|
|
||||||
new_server.server_password = settings.password
|
|
||||||
|
|
||||||
settings.server_preset_interface = settings.server_name
|
|
||||||
|
|
||||||
if new_server == existing_preset :
|
|
||||||
self.report({'INFO'}, "Server '" + settings.server_name + "' override")
|
|
||||||
else :
|
|
||||||
self.report({'INFO'}, "New '" + settings.server_name + "' server preset")
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class SessionPresetServerRemove(bpy.types.Operator):
|
class SessionPresetServerRemove(bpy.types.Operator):
|
||||||
"""Remove a server to the server list preset"""
|
"""Remove a server to the server list preset"""
|
||||||
bl_idname = "session.preset_server_remove"
|
bl_idname = "session.preset_server_remove"
|
||||||
@ -884,6 +1029,8 @@ class SessionPresetServerRemove(bpy.types.Operator):
|
|||||||
bl_description = "remove the current server from the server preset list"
|
bl_description = "remove the current server from the server preset list"
|
||||||
bl_options = {"REGISTER"}
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
|
target_server_name: bpy.props.StringProperty(default="None")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return True
|
return True
|
||||||
@ -892,19 +1039,77 @@ class SessionPresetServerRemove(bpy.types.Operator):
|
|||||||
assert(context)
|
assert(context)
|
||||||
|
|
||||||
settings = utils.get_preferences()
|
settings = utils.get_preferences()
|
||||||
|
settings.server_preset.remove(settings.server_preset.find(self.target_server_name))
|
||||||
settings.server_preset.remove(settings.server_preset.find(settings.server_preset_interface))
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class RefreshServerStatus(bpy.types.Operator):
|
||||||
|
bl_idname = "session.get_info"
|
||||||
|
bl_label = "Get session info"
|
||||||
|
bl_description = "Get session info"
|
||||||
|
|
||||||
|
target_server: bpy.props.StringProperty(default="127.0.0.1:5555")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return (session.state != STATE_ACTIVE)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
settings = utils.get_preferences()
|
||||||
|
|
||||||
|
for server in settings.server_preset:
|
||||||
|
infos = porcelain.request_session_info(f"{server.ip}:{server.port}", timeout=settings.ping_timeout)
|
||||||
|
server.is_online = True if infos else False
|
||||||
|
if server.is_online:
|
||||||
|
server.is_private = infos.get("private")
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class GetDoc(bpy.types.Operator):
|
||||||
|
"""Get the documentation of the addon"""
|
||||||
|
bl_idname = "doc.get"
|
||||||
|
bl_label = "Multi-user's doc"
|
||||||
|
bl_description = "Go to the doc of the addon"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
assert(context)
|
||||||
|
bpy.ops.wm.url_open(url="https://slumber.gitlab.io/multi-user/index.html")
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class FirstLaunch(bpy.types.Operator):
|
||||||
|
"""First time lauching the addon"""
|
||||||
|
bl_idname = "firstlaunch.verify"
|
||||||
|
bl_label = "First launch"
|
||||||
|
bl_description = "First time lauching the addon"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
assert(context)
|
||||||
|
settings = utils.get_preferences()
|
||||||
|
settings.is_first_launch = False
|
||||||
|
settings.server_preset.clear()
|
||||||
|
prefs = bpy.context.preferences.addons[__package__].preferences
|
||||||
|
prefs.generate_default_presets()
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
def menu_func_import(self, context):
|
def menu_func_import(self, context):
|
||||||
self.layout.operator(SessionLoadSaveOperator.bl_idname, text='Multi-user session snapshot (.db)')
|
self.layout.operator(SessionLoadSaveOperator.bl_idname, text='Multi-user session snapshot (.db)')
|
||||||
|
|
||||||
|
def menu_func_export(self, context):
|
||||||
|
self.layout.operator(SessionSaveBackupOperator.bl_idname, text='Multi-user session snapshot (.db)')
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
SessionStartOperator,
|
SessionConnectOperator,
|
||||||
|
SessionHostOperator,
|
||||||
SessionStopOperator,
|
SessionStopOperator,
|
||||||
SessionPropertyRemoveOperator,
|
SessionPropertyRemoveOperator,
|
||||||
SessionSnapUserOperator,
|
SessionSnapUserOperator,
|
||||||
@ -921,7 +1126,11 @@ classes = (
|
|||||||
SessionStopAutoSaveOperator,
|
SessionStopAutoSaveOperator,
|
||||||
SessionPurgeOperator,
|
SessionPurgeOperator,
|
||||||
SessionPresetServerAdd,
|
SessionPresetServerAdd,
|
||||||
|
SessionPresetServerEdit,
|
||||||
SessionPresetServerRemove,
|
SessionPresetServerRemove,
|
||||||
|
RefreshServerStatus,
|
||||||
|
GetDoc,
|
||||||
|
FirstLaunch,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
import random
|
import random
|
||||||
import logging
|
import logging
|
||||||
|
from uuid import uuid4
|
||||||
import bpy
|
import bpy
|
||||||
import string
|
import string
|
||||||
import re
|
import re
|
||||||
@ -25,7 +26,7 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from . import bl_types, environment, addon_updater_ops, presence, ui
|
from . import bl_types, environment, addon_updater_ops, presence, ui
|
||||||
from .utils import get_preferences, get_expanded_icon
|
from .utils import get_preferences, get_expanded_icon, get_folder_size
|
||||||
from replication.constants import RP_COMMON
|
from replication.constants import RP_COMMON
|
||||||
from replication.interface import session
|
from replication.interface import session
|
||||||
|
|
||||||
@ -33,15 +34,21 @@ from replication.interface import session
|
|||||||
IP_REGEX = re.compile("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$")
|
IP_REGEX = re.compile("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$")
|
||||||
HOSTNAME_REGEX = re.compile("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$")
|
HOSTNAME_REGEX = re.compile("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$")
|
||||||
|
|
||||||
|
#SERVER PRESETS AT LAUNCH
|
||||||
DEFAULT_PRESETS = {
|
DEFAULT_PRESETS = {
|
||||||
"localhost" : {
|
"localhost" : {
|
||||||
"server_ip": "localhost",
|
"server_name": "localhost",
|
||||||
"server_port": 5555,
|
"ip": "localhost",
|
||||||
"server_password": "admin"
|
"port": 5555,
|
||||||
|
"use_admin_password": True,
|
||||||
|
"admin_password": "admin",
|
||||||
|
"server_password": ""
|
||||||
},
|
},
|
||||||
"public session" : {
|
"public session" : {
|
||||||
"server_ip": "51.75.71.183",
|
"server_name": "public session",
|
||||||
"server_port": 5555,
|
"ip": "51.75.71.183",
|
||||||
|
"port": 5555,
|
||||||
|
"admin_password": "",
|
||||||
"server_password": ""
|
"server_password": ""
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -78,11 +85,6 @@ def update_ip(self, context):
|
|||||||
logging.error("Wrong IP format")
|
logging.error("Wrong IP format")
|
||||||
self['ip'] = "127.0.0.1"
|
self['ip'] = "127.0.0.1"
|
||||||
|
|
||||||
def update_server_preset_interface(self, context):
|
|
||||||
self.server_name = self.server_preset.get(self.server_preset_interface).name
|
|
||||||
self.ip = self.server_preset.get(self.server_preset_interface).server_ip
|
|
||||||
self.port = self.server_preset.get(self.server_preset_interface).server_port
|
|
||||||
self.password = self.server_preset.get(self.server_preset_interface).server_password
|
|
||||||
|
|
||||||
def update_directory(self, context):
|
def update_directory(self, context):
|
||||||
new_dir = Path(self.cache_directory)
|
new_dir = Path(self.cache_directory)
|
||||||
@ -110,9 +112,15 @@ class ReplicatedDatablock(bpy.types.PropertyGroup):
|
|||||||
icon: bpy.props.StringProperty()
|
icon: bpy.props.StringProperty()
|
||||||
|
|
||||||
class ServerPreset(bpy.types.PropertyGroup):
|
class ServerPreset(bpy.types.PropertyGroup):
|
||||||
server_ip: bpy.props.StringProperty()
|
server_name: bpy.props.StringProperty(default="")
|
||||||
server_port: bpy.props.IntProperty(default=5555)
|
ip: bpy.props.StringProperty(default="127.0.0.1", update=update_ip)
|
||||||
server_password: bpy.props.StringProperty(default="admin", subtype = "PASSWORD")
|
port: bpy.props.IntProperty(default=5555)
|
||||||
|
use_server_password: bpy.props.BoolProperty(default=False)
|
||||||
|
server_password: bpy.props.StringProperty(default="", subtype = "PASSWORD")
|
||||||
|
use_admin_password: bpy.props.BoolProperty(default=False)
|
||||||
|
admin_password: bpy.props.StringProperty(default="", subtype = "PASSWORD")
|
||||||
|
is_online: bpy.props.BoolProperty(default=False)
|
||||||
|
is_private: bpy.props.BoolProperty(default=False)
|
||||||
|
|
||||||
def set_sync_render_settings(self, value):
|
def set_sync_render_settings(self, value):
|
||||||
self['sync_render_settings'] = value
|
self['sync_render_settings'] = value
|
||||||
@ -162,35 +170,60 @@ class ReplicationFlags(bpy.types.PropertyGroup):
|
|||||||
class SessionPrefs(bpy.types.AddonPreferences):
|
class SessionPrefs(bpy.types.AddonPreferences):
|
||||||
bl_idname = __package__
|
bl_idname = __package__
|
||||||
|
|
||||||
ip: bpy.props.StringProperty(
|
# User settings
|
||||||
name="ip",
|
|
||||||
description='Distant host ip',
|
|
||||||
default="localhost",
|
|
||||||
update=update_ip)
|
|
||||||
username: bpy.props.StringProperty(
|
username: bpy.props.StringProperty(
|
||||||
name="Username",
|
name="Username",
|
||||||
default=f"user_{random_string_digits()}"
|
default=f"user_{random_string_digits()}"
|
||||||
)
|
)
|
||||||
client_color: bpy.props.FloatVectorProperty(
|
client_color: bpy.props.FloatVectorProperty(
|
||||||
name="client_instance_color",
|
name="client_instance_color",
|
||||||
|
description='User color',
|
||||||
subtype='COLOR',
|
subtype='COLOR',
|
||||||
default=randomColor())
|
default=randomColor()
|
||||||
port: bpy.props.IntProperty(
|
|
||||||
name="port",
|
|
||||||
description='Distant host port',
|
|
||||||
default=5555
|
|
||||||
)
|
)
|
||||||
|
# Current server settings
|
||||||
server_name: bpy.props.StringProperty(
|
server_name: bpy.props.StringProperty(
|
||||||
name="server_name",
|
name="server_name",
|
||||||
description="Custom name of the server",
|
description="Custom name of the server",
|
||||||
default='localhost',
|
default='localhost',
|
||||||
)
|
)
|
||||||
password: bpy.props.StringProperty(
|
server_index: bpy.props.IntProperty(
|
||||||
name="password",
|
name="server_index",
|
||||||
default=random_string_digits(),
|
description="index of the server",
|
||||||
|
)
|
||||||
|
# User host session settings
|
||||||
|
host_port: bpy.props.IntProperty(
|
||||||
|
name="host_port",
|
||||||
|
description='Distant host port',
|
||||||
|
default=5555
|
||||||
|
)
|
||||||
|
host_use_server_password: bpy.props.BoolProperty(
|
||||||
|
name="use_server_password",
|
||||||
|
description='Use session password',
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
host_server_password: bpy.props.StringProperty(
|
||||||
|
name="server_password",
|
||||||
description='Session password',
|
description='Session password',
|
||||||
subtype='PASSWORD'
|
subtype='PASSWORD'
|
||||||
)
|
)
|
||||||
|
host_use_admin_password: bpy.props.BoolProperty(
|
||||||
|
name="use_admin_password",
|
||||||
|
description='Use admin password',
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
host_admin_password: bpy.props.StringProperty(
|
||||||
|
name="admin_password",
|
||||||
|
description='Admin password',
|
||||||
|
subtype='PASSWORD',
|
||||||
|
default='admin'
|
||||||
|
)
|
||||||
|
# Other
|
||||||
|
is_first_launch: bpy.props.BoolProperty(
|
||||||
|
name="is_fnirst_launch",
|
||||||
|
description="First time lauching the addon",
|
||||||
|
default=True
|
||||||
|
)
|
||||||
sync_flags: bpy.props.PointerProperty(
|
sync_flags: bpy.props.PointerProperty(
|
||||||
type=ReplicationFlags
|
type=ReplicationFlags
|
||||||
)
|
)
|
||||||
@ -214,6 +247,11 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
|||||||
description='connection timeout before disconnection',
|
description='connection timeout before disconnection',
|
||||||
default=5000
|
default=5000
|
||||||
)
|
)
|
||||||
|
ping_timeout: bpy.props.IntProperty(
|
||||||
|
name='ping timeout',
|
||||||
|
description='check if servers are online',
|
||||||
|
default=500
|
||||||
|
)
|
||||||
# Replication update settings
|
# Replication update settings
|
||||||
depsgraph_update_rate: bpy.props.FloatProperty(
|
depsgraph_update_rate: bpy.props.FloatProperty(
|
||||||
name='depsgraph update rate (s)',
|
name='depsgraph update rate (s)',
|
||||||
@ -225,11 +263,12 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
|||||||
description="Remove filecache from memory",
|
description="Remove filecache from memory",
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
# for UI
|
# For UI
|
||||||
category: bpy.props.EnumProperty(
|
category: bpy.props.EnumProperty(
|
||||||
name="Category",
|
name="Category",
|
||||||
description="Preferences Category",
|
description="Preferences Category",
|
||||||
items=[
|
items=[
|
||||||
|
('PREF', "Preferences", "Preferences of this add-on"),
|
||||||
('CONFIG', "Configuration", "Configuration of this add-on"),
|
('CONFIG', "Configuration", "Configuration of this add-on"),
|
||||||
('UPDATE', "Update", "Update this add-on"),
|
('UPDATE', "Update", "Update this add-on"),
|
||||||
],
|
],
|
||||||
@ -273,31 +312,58 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
|||||||
step=1,
|
step=1,
|
||||||
subtype='PERCENTAGE',
|
subtype='PERCENTAGE',
|
||||||
)
|
)
|
||||||
|
presence_text_distance: bpy.props.FloatProperty(
|
||||||
|
name="Distance text visibilty",
|
||||||
|
description="Adjust the distance visibilty of user's mode/name",
|
||||||
|
min=0.1,
|
||||||
|
max=10000,
|
||||||
|
default=100,
|
||||||
|
)
|
||||||
conf_session_identity_expanded: bpy.props.BoolProperty(
|
conf_session_identity_expanded: bpy.props.BoolProperty(
|
||||||
name="Identity",
|
name="Identity",
|
||||||
description="Identity",
|
description="Identity",
|
||||||
default=True
|
default=False
|
||||||
)
|
)
|
||||||
conf_session_net_expanded: bpy.props.BoolProperty(
|
conf_session_net_expanded: bpy.props.BoolProperty(
|
||||||
name="Net",
|
name="Net",
|
||||||
description="net",
|
description="net",
|
||||||
default=True
|
default=False
|
||||||
)
|
)
|
||||||
conf_session_hosting_expanded: bpy.props.BoolProperty(
|
conf_session_hosting_expanded: bpy.props.BoolProperty(
|
||||||
name="Rights",
|
name="Rights",
|
||||||
description="Rights",
|
description="Rights",
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
conf_session_rep_expanded: bpy.props.BoolProperty(
|
||||||
|
name="Replication",
|
||||||
|
description="Replication",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
conf_session_cache_expanded: bpy.props.BoolProperty(
|
conf_session_cache_expanded: bpy.props.BoolProperty(
|
||||||
name="Cache",
|
name="Cache",
|
||||||
description="cache",
|
description="cache",
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
conf_session_log_expanded: bpy.props.BoolProperty(
|
||||||
|
name="conf_session_log_expanded",
|
||||||
|
description="conf_session_log_expanded",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
conf_session_ui_expanded: bpy.props.BoolProperty(
|
conf_session_ui_expanded: bpy.props.BoolProperty(
|
||||||
name="Interface",
|
name="Interface",
|
||||||
description="Interface",
|
description="Interface",
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
sidebar_repository_shown: bpy.props.BoolProperty(
|
||||||
|
name="sidebar_repository_shown",
|
||||||
|
description="sidebar_repository_shown",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
sidebar_advanced_shown: bpy.props.BoolProperty(
|
||||||
|
name="sidebar_advanced_shown",
|
||||||
|
description="sidebar_advanced_shown",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
sidebar_advanced_rep_expanded: bpy.props.BoolProperty(
|
sidebar_advanced_rep_expanded: bpy.props.BoolProperty(
|
||||||
name="sidebar_advanced_rep_expanded",
|
name="sidebar_advanced_rep_expanded",
|
||||||
description="sidebar_advanced_rep_expanded",
|
description="sidebar_advanced_rep_expanded",
|
||||||
@ -308,6 +374,11 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
|||||||
description="sidebar_advanced_log_expanded",
|
description="sidebar_advanced_log_expanded",
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
sidebar_advanced_hosting_expanded: bpy.props.BoolProperty(
|
||||||
|
name="sidebar_advanced_hosting_expanded",
|
||||||
|
description="sidebar_advanced_hosting_expanded",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
sidebar_advanced_net_expanded: bpy.props.BoolProperty(
|
sidebar_advanced_net_expanded: bpy.props.BoolProperty(
|
||||||
name="sidebar_advanced_net_expanded",
|
name="sidebar_advanced_net_expanded",
|
||||||
description="sidebar_advanced_net_expanded",
|
description="sidebar_advanced_net_expanded",
|
||||||
@ -364,12 +435,6 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
|||||||
name="server preset",
|
name="server preset",
|
||||||
type=ServerPreset,
|
type=ServerPreset,
|
||||||
)
|
)
|
||||||
server_preset_interface: bpy.props.EnumProperty(
|
|
||||||
name="servers",
|
|
||||||
description="servers enum",
|
|
||||||
items=server_list_callback,
|
|
||||||
update=update_server_preset_interface,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Custom panel
|
# Custom panel
|
||||||
panel_category: bpy.props.StringProperty(
|
panel_category: bpy.props.StringProperty(
|
||||||
@ -379,38 +444,28 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
|||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
|
||||||
layout.row().prop(self, "category", expand=True)
|
layout.row().prop(self, "category", expand=True)
|
||||||
|
|
||||||
|
if self.category == 'PREF':
|
||||||
|
grid = layout.column()
|
||||||
|
|
||||||
|
box = grid.box()
|
||||||
|
row = box.row()
|
||||||
|
# USER SETTINGS
|
||||||
|
split = row.split(factor=0.7, align=True)
|
||||||
|
split.prop(self, "username", text="User")
|
||||||
|
split.prop(self, "client_color", text="")
|
||||||
|
|
||||||
|
row = box.row()
|
||||||
|
row.label(text="Hide settings:")
|
||||||
|
row = box.row()
|
||||||
|
row.prop(self, "sidebar_advanced_shown", text="Hide “Advanced” settings in side pannel (Not in session)")
|
||||||
|
row = box.row()
|
||||||
|
row.prop(self, "sidebar_repository_shown", text="Hide “Repository” settings in side pannel (In session)")
|
||||||
|
|
||||||
if self.category == 'CONFIG':
|
if self.category == 'CONFIG':
|
||||||
grid = layout.column()
|
grid = layout.column()
|
||||||
|
|
||||||
# USER INFORMATIONS
|
|
||||||
box = grid.box()
|
|
||||||
box.prop(
|
|
||||||
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:
|
|
||||||
box.row().prop(self, "username", text="name")
|
|
||||||
box.row().prop(self, "client_color", text="color")
|
|
||||||
|
|
||||||
# NETWORK SETTINGS
|
|
||||||
box = grid.box()
|
|
||||||
box.prop(
|
|
||||||
self, "conf_session_net_expanded", text="Networking",
|
|
||||||
icon=get_expanded_icon(self.conf_session_net_expanded),
|
|
||||||
emboss=False)
|
|
||||||
|
|
||||||
if self.conf_session_net_expanded:
|
|
||||||
box.row().prop(self, "ip", text="Address")
|
|
||||||
row = box.row()
|
|
||||||
row.label(text="Port:")
|
|
||||||
row.prop(self, "port", text="")
|
|
||||||
row = box.row()
|
|
||||||
row.label(text="Init the session from:")
|
|
||||||
row.prop(self, "init_method", text="")
|
|
||||||
|
|
||||||
# HOST SETTINGS
|
# HOST SETTINGS
|
||||||
box = grid.box()
|
box = grid.box()
|
||||||
box.prop(
|
box.prop(
|
||||||
@ -418,9 +473,57 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
|||||||
icon=get_expanded_icon(self.conf_session_hosting_expanded),
|
icon=get_expanded_icon(self.conf_session_hosting_expanded),
|
||||||
emboss=False)
|
emboss=False)
|
||||||
if self.conf_session_hosting_expanded:
|
if self.conf_session_hosting_expanded:
|
||||||
|
row = box.row()
|
||||||
|
row.prop(self, "host_port", text="Port: ")
|
||||||
row = box.row()
|
row = box.row()
|
||||||
row.label(text="Init the session from:")
|
row.label(text="Init the session from:")
|
||||||
row.prop(self, "init_method", text="")
|
row.prop(self, "init_method", text="")
|
||||||
|
row = box.row()
|
||||||
|
col = row.column()
|
||||||
|
col.prop(self, "host_use_server_password", text="Server password:")
|
||||||
|
col = row.column()
|
||||||
|
col.enabled = True if self.host_use_server_password else False
|
||||||
|
col.prop(self, "host_server_password", text="")
|
||||||
|
row = box.row()
|
||||||
|
col = row.column()
|
||||||
|
col.prop(self, "host_use_admin_password", text="Admin password:")
|
||||||
|
col = row.column()
|
||||||
|
col.enabled = True if self.host_use_admin_password else False
|
||||||
|
col.prop(self, "host_admin_password", text="")
|
||||||
|
|
||||||
|
# NETWORKING
|
||||||
|
box = grid.box()
|
||||||
|
box.prop(
|
||||||
|
self, "conf_session_net_expanded", text="Network",
|
||||||
|
icon=get_expanded_icon(self.conf_session_net_expanded),
|
||||||
|
emboss=False)
|
||||||
|
if self.conf_session_net_expanded:
|
||||||
|
row = box.row()
|
||||||
|
row.label(text="Timeout (ms):")
|
||||||
|
row.prop(self, "connection_timeout", text="")
|
||||||
|
row = box.row()
|
||||||
|
row.label(text="Server ping (ms):")
|
||||||
|
row.prop(self, "ping_timeout", text="")
|
||||||
|
|
||||||
|
# REPLICATION
|
||||||
|
box = grid.box()
|
||||||
|
box.prop(
|
||||||
|
self, "conf_session_rep_expanded", text="Replication",
|
||||||
|
icon=get_expanded_icon(self.conf_session_rep_expanded),
|
||||||
|
emboss=False)
|
||||||
|
if self.conf_session_rep_expanded:
|
||||||
|
row = box.row()
|
||||||
|
row.prop(self.sync_flags, "sync_render_settings")
|
||||||
|
row = box.row()
|
||||||
|
row.prop(self.sync_flags, "sync_active_camera")
|
||||||
|
row = box.row()
|
||||||
|
row.prop(self.sync_flags, "sync_during_editmode")
|
||||||
|
row = box.row()
|
||||||
|
if self.sync_flags.sync_during_editmode:
|
||||||
|
warning = row.box()
|
||||||
|
warning.label(text="Don't use this with heavy meshes !", icon='ERROR')
|
||||||
|
row = box.row()
|
||||||
|
row.prop(self, "depsgraph_update_rate", text="Apply delay")
|
||||||
|
|
||||||
# CACHE SETTINGS
|
# CACHE SETTINGS
|
||||||
box = grid.box()
|
box = grid.box()
|
||||||
@ -431,24 +534,18 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
|||||||
if self.conf_session_cache_expanded:
|
if self.conf_session_cache_expanded:
|
||||||
box.row().prop(self, "cache_directory", text="Cache directory")
|
box.row().prop(self, "cache_directory", text="Cache directory")
|
||||||
box.row().prop(self, "clear_memory_filecache", text="Clear memory filecache")
|
box.row().prop(self, "clear_memory_filecache", text="Clear memory filecache")
|
||||||
|
box.row().operator('session.clear_cache', text=f"Clear cache ({get_folder_size(self.cache_directory)})")
|
||||||
# INTERFACE SETTINGS
|
|
||||||
|
# LOGGING
|
||||||
box = grid.box()
|
box = grid.box()
|
||||||
box.prop(
|
box.prop(
|
||||||
self, "conf_session_ui_expanded", text="Interface",
|
self, "conf_session_log_expanded", text="Logging",
|
||||||
icon=get_expanded_icon(self.conf_session_ui_expanded),
|
icon=get_expanded_icon(self.conf_session_log_expanded),
|
||||||
emboss=False)
|
emboss=False)
|
||||||
if self.conf_session_ui_expanded:
|
if self.conf_session_log_expanded:
|
||||||
box.row().prop(self, "panel_category", text="Panel category", expand=True)
|
|
||||||
row = box.row()
|
row = box.row()
|
||||||
row.label(text="Session widget:")
|
row.label(text="Log level:")
|
||||||
|
row.prop(self, 'logging_level', text="")
|
||||||
col = box.column(align=True)
|
|
||||||
col.prop(self, "presence_hud_scale", expand=True)
|
|
||||||
|
|
||||||
|
|
||||||
col.prop(self, "presence_hud_hpos", expand=True)
|
|
||||||
col.prop(self, "presence_hud_vpos", expand=True)
|
|
||||||
|
|
||||||
if self.category == 'UPDATE':
|
if self.category == 'UPDATE':
|
||||||
from . import addon_updater_ops
|
from . import addon_updater_ops
|
||||||
@ -469,18 +566,31 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
|||||||
new_db.icon = impl.bl_icon
|
new_db.icon = impl.bl_icon
|
||||||
new_db.bl_name = impl.bl_id
|
new_db.bl_name = impl.bl_id
|
||||||
|
|
||||||
|
# Get a server preset through its name
|
||||||
|
def get_server_preset(self, name):
|
||||||
|
existing_preset = None
|
||||||
|
|
||||||
# custom at launch server preset
|
for server_preset in self.server_preset :
|
||||||
|
if server_preset.server_name == name :
|
||||||
|
existing_preset = server_preset
|
||||||
|
|
||||||
|
return existing_preset
|
||||||
|
|
||||||
|
# Custom at launch server preset
|
||||||
def generate_default_presets(self):
|
def generate_default_presets(self):
|
||||||
for preset_name, preset_data in DEFAULT_PRESETS.items():
|
for preset_name, preset_data in DEFAULT_PRESETS.items():
|
||||||
existing_preset = self.server_preset.get(preset_name)
|
existing_preset = self.get_server_preset(preset_name)
|
||||||
if existing_preset :
|
if existing_preset :
|
||||||
continue
|
continue
|
||||||
new_server = self.server_preset.add()
|
new_server = self.server_preset.add()
|
||||||
new_server.name = preset_name
|
new_server.name = str(uuid4())
|
||||||
new_server.server_ip = preset_data.get('server_ip')
|
new_server.server_name = preset_data.get('server_name')
|
||||||
new_server.server_port = preset_data.get('server_port')
|
new_server.ip = preset_data.get('ip')
|
||||||
|
new_server.port = preset_data.get('port')
|
||||||
|
new_server.use_server_password = preset_data.get('use_server_password',False)
|
||||||
new_server.server_password = preset_data.get('server_password',None)
|
new_server.server_password = preset_data.get('server_password',None)
|
||||||
|
new_server.use_admin_password = preset_data.get('use_admin_password',False)
|
||||||
|
new_server.admin_password = preset_data.get('admin_password',None)
|
||||||
|
|
||||||
|
|
||||||
def client_list_callback(scene, context):
|
def client_list_callback(scene, context):
|
||||||
@ -538,6 +648,11 @@ class SessionProps(bpy.types.PropertyGroup):
|
|||||||
description='Enable user overlay ',
|
description='Enable user overlay ',
|
||||||
default=True,
|
default=True,
|
||||||
)
|
)
|
||||||
|
presence_show_mode: bpy.props.BoolProperty(
|
||||||
|
name="Show users current mode",
|
||||||
|
description='Enable user mode overlay ',
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
presence_show_far_user: bpy.props.BoolProperty(
|
presence_show_far_user: bpy.props.BoolProperty(
|
||||||
name="Show users on different scenes",
|
name="Show users on different scenes",
|
||||||
description="Show user on different scenes",
|
description="Show user on different scenes",
|
||||||
@ -563,11 +678,6 @@ class SessionProps(bpy.types.PropertyGroup):
|
|||||||
description='Connect as admin',
|
description='Connect as admin',
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
internet_ip: bpy.props.StringProperty(
|
|
||||||
name="internet ip",
|
|
||||||
default="no found",
|
|
||||||
description='Internet interface ip',
|
|
||||||
)
|
|
||||||
user_snap_running: bpy.props.BoolProperty(
|
user_snap_running: bpy.props.BoolProperty(
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
@ -94,15 +94,41 @@ def project_to_viewport(region: bpy.types.Region, rv3d: bpy.types.RegionView3D,
|
|||||||
return [target.x, target.y, target.z]
|
return [target.x, target.y, target.z]
|
||||||
|
|
||||||
|
|
||||||
def bbox_from_obj(obj: bpy.types.Object, radius: float) -> list:
|
def bbox_from_obj(obj: bpy.types.Object, index: int = 1) -> list:
|
||||||
""" Generate a bounding box for a given object by using its world matrix
|
""" Generate a bounding box for a given object by using its world matrix
|
||||||
|
|
||||||
:param obj: target object
|
:param obj: target object
|
||||||
:type obj: bpy.types.Object
|
:type obj: bpy.types.Object
|
||||||
:param radius: bounding box radius
|
:param index: indice offset
|
||||||
:type radius: float
|
:type index: int
|
||||||
:return: list of 8 points [(x,y,z),...]
|
:return: list of 8 points [(x,y,z),...], list of 12 link between these points [(1,2),...]
|
||||||
"""
|
"""
|
||||||
|
radius = 1.0 # Radius of the bounding box
|
||||||
|
index = 8*index
|
||||||
|
vertex_indices = (
|
||||||
|
(0+index, 1+index), (0+index, 2+index), (1+index, 3+index), (2+index, 3+index),
|
||||||
|
(4+index, 5+index), (4+index, 6+index), (5+index, 7+index), (6+index, 7+index),
|
||||||
|
(0+index, 4+index), (1+index, 5+index), (2+index, 6+index), (3+index, 7+index))
|
||||||
|
|
||||||
|
if obj.type == 'EMPTY':
|
||||||
|
radius = obj.empty_display_size
|
||||||
|
elif obj.type == 'LIGHT':
|
||||||
|
radius = obj.data.shadow_soft_size
|
||||||
|
elif obj.type == 'LIGHT_PROBE':
|
||||||
|
radius = obj.data.influence_distance
|
||||||
|
elif obj.type == 'CAMERA':
|
||||||
|
radius = obj.data.display_size
|
||||||
|
elif hasattr(obj, 'bound_box'):
|
||||||
|
vertex_indices = (
|
||||||
|
(0+index, 1+index), (1+index, 2+index),
|
||||||
|
(2+index, 3+index), (0+index, 3+index),
|
||||||
|
(4+index, 5+index), (5+index, 6+index),
|
||||||
|
(6+index, 7+index), (4+index, 7+index),
|
||||||
|
(0+index, 4+index), (1+index, 5+index),
|
||||||
|
(2+index, 6+index), (3+index, 7+index))
|
||||||
|
vertex_pos = get_bb_coords_from_obj(obj)
|
||||||
|
return vertex_pos, vertex_indices
|
||||||
|
|
||||||
coords = [
|
coords = [
|
||||||
(-radius, -radius, -radius), (+radius, -radius, -radius),
|
(-radius, -radius, -radius), (+radius, -radius, -radius),
|
||||||
(-radius, +radius, -radius), (+radius, +radius, -radius),
|
(-radius, +radius, -radius), (+radius, +radius, -radius),
|
||||||
@ -112,9 +138,32 @@ def bbox_from_obj(obj: bpy.types.Object, radius: float) -> list:
|
|||||||
base = obj.matrix_world
|
base = obj.matrix_world
|
||||||
bbox_corners = [base @ mathutils.Vector(corner) for corner in coords]
|
bbox_corners = [base @ mathutils.Vector(corner) for corner in coords]
|
||||||
|
|
||||||
return [(point.x, point.y, point.z)
|
vertex_pos = [(point.x, point.y, point.z) for point in bbox_corners]
|
||||||
for point in bbox_corners]
|
|
||||||
|
|
||||||
|
return vertex_pos, vertex_indices
|
||||||
|
|
||||||
|
def bbox_from_instance_collection(ic: bpy.types.Object, index: int = 0) -> list:
|
||||||
|
""" Generate a bounding box for a given instance collection by using its objects
|
||||||
|
|
||||||
|
:param ic: target instance collection
|
||||||
|
:type ic: bpy.types.Object
|
||||||
|
:param index: indice offset
|
||||||
|
:type index: int
|
||||||
|
:return: list of 8*objs points [(x,y,z),...], tuple of 12*objs link between these points [(1,2),...]
|
||||||
|
"""
|
||||||
|
vertex_pos = []
|
||||||
|
vertex_indices = ()
|
||||||
|
|
||||||
|
for obj_index, obj in enumerate(ic.instance_collection.objects):
|
||||||
|
vertex_pos_temp, vertex_indices_temp = bbox_from_obj(obj, index=index+obj_index)
|
||||||
|
vertex_pos += vertex_pos_temp
|
||||||
|
vertex_indices += vertex_indices_temp
|
||||||
|
|
||||||
|
bbox_corners = [ic.matrix_world @ mathutils.Vector(vertex) for vertex in vertex_pos]
|
||||||
|
|
||||||
|
vertex_pos = [(point.x, point.y, point.z) for point in bbox_corners]
|
||||||
|
|
||||||
|
return vertex_pos, vertex_indices
|
||||||
|
|
||||||
def generate_user_camera() -> list:
|
def generate_user_camera() -> list:
|
||||||
""" Generate a basic camera represention of the user point of view
|
""" Generate a basic camera represention of the user point of view
|
||||||
@ -175,7 +224,7 @@ def get_bb_coords_from_obj(object: bpy.types.Object, instance: bpy.types.Object
|
|||||||
|
|
||||||
bbox_corners = [base @ mathutils.Vector(
|
bbox_corners = [base @ mathutils.Vector(
|
||||||
corner) for corner in object.bound_box]
|
corner) for corner in object.bound_box]
|
||||||
|
|
||||||
|
|
||||||
return [(point.x, point.y, point.z) for point in bbox_corners]
|
return [(point.x, point.y, point.z) for point in bbox_corners]
|
||||||
|
|
||||||
@ -203,6 +252,13 @@ class Widget(object):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def configure_bgl(self):
|
||||||
|
bgl.glLineWidth(2.)
|
||||||
|
bgl.glEnable(bgl.GL_DEPTH_TEST)
|
||||||
|
bgl.glEnable(bgl.GL_BLEND)
|
||||||
|
bgl.glEnable(bgl.GL_LINE_SMOOTH)
|
||||||
|
|
||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
"""How to draw the widget
|
"""How to draw the widget
|
||||||
"""
|
"""
|
||||||
@ -256,11 +312,6 @@ class UserFrustumWidget(Widget):
|
|||||||
{"pos": positions},
|
{"pos": positions},
|
||||||
indices=self.indices)
|
indices=self.indices)
|
||||||
|
|
||||||
bgl.glLineWidth(2.)
|
|
||||||
bgl.glEnable(bgl.GL_DEPTH_TEST)
|
|
||||||
bgl.glEnable(bgl.GL_BLEND)
|
|
||||||
bgl.glEnable(bgl.GL_LINE_SMOOTH)
|
|
||||||
|
|
||||||
shader.bind()
|
shader.bind()
|
||||||
shader.uniform_float("color", self.data.get('color'))
|
shader.uniform_float("color", self.data.get('color'))
|
||||||
batch.draw(shader)
|
batch.draw(shader)
|
||||||
@ -272,6 +323,8 @@ class UserSelectionWidget(Widget):
|
|||||||
username):
|
username):
|
||||||
self.username = username
|
self.username = username
|
||||||
self.settings = bpy.context.window_manager.session
|
self.settings = bpy.context.window_manager.session
|
||||||
|
self.current_selection_ids = []
|
||||||
|
self.current_selected_objects = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data(self):
|
def data(self):
|
||||||
@ -281,6 +334,15 @@ class UserSelectionWidget(Widget):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def selected_objects(self):
|
||||||
|
user_selection = self.data.get('selected_objects')
|
||||||
|
if self.current_selection_ids != user_selection:
|
||||||
|
self.current_selected_objects = [find_from_attr("uuid", uid, bpy.data.objects) for uid in user_selection]
|
||||||
|
self.current_selection_ids = user_selection
|
||||||
|
|
||||||
|
return self.current_selected_objects
|
||||||
|
|
||||||
def poll(self):
|
def poll(self):
|
||||||
if self.data is None:
|
if self.data is None:
|
||||||
return False
|
return False
|
||||||
@ -295,49 +357,31 @@ class UserSelectionWidget(Widget):
|
|||||||
self.settings.enable_presence
|
self.settings.enable_presence
|
||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
user_selection = self.data.get('selected_objects')
|
vertex_pos = []
|
||||||
for select_ob in user_selection:
|
vertex_ind = []
|
||||||
ob = find_from_attr("uuid", select_ob, bpy.data.objects)
|
collection_offset = 0
|
||||||
if not ob:
|
for obj_index, obj in enumerate(self.selected_objects):
|
||||||
return
|
if obj is None:
|
||||||
|
continue
|
||||||
|
obj_index+=collection_offset
|
||||||
|
if hasattr(obj, 'instance_collection') and obj.instance_collection:
|
||||||
|
bbox_pos, bbox_ind = bbox_from_instance_collection(obj, index=obj_index)
|
||||||
|
collection_offset+=len(obj.instance_collection.objects)-1
|
||||||
|
else :
|
||||||
|
bbox_pos, bbox_ind = bbox_from_obj(obj, index=obj_index)
|
||||||
|
vertex_pos += bbox_pos
|
||||||
|
vertex_ind += bbox_ind
|
||||||
|
|
||||||
vertex_pos = bbox_from_obj(ob, 1.0)
|
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
|
||||||
vertex_indices = (
|
batch = batch_for_shader(
|
||||||
(0, 1), (1, 2), (2, 3), (0, 3),
|
shader,
|
||||||
(4, 5), (5, 6), (6, 7), (4, 7),
|
'LINES',
|
||||||
(0, 4), (1, 5), (2, 6), (3, 7))
|
{"pos": vertex_pos},
|
||||||
|
indices=vertex_ind)
|
||||||
if ob.instance_collection:
|
|
||||||
for obj in ob.instance_collection.objects:
|
|
||||||
if obj.type == 'MESH' and hasattr(obj, 'bound_box'):
|
|
||||||
vertex_pos = get_bb_coords_from_obj(obj, instance=ob)
|
|
||||||
break
|
|
||||||
elif ob.type == 'EMPTY':
|
|
||||||
vertex_pos = bbox_from_obj(ob, ob.empty_display_size)
|
|
||||||
elif ob.type == 'LIGHT':
|
|
||||||
vertex_pos = bbox_from_obj(ob, ob.data.shadow_soft_size)
|
|
||||||
elif ob.type == 'LIGHT_PROBE':
|
|
||||||
vertex_pos = bbox_from_obj(ob, ob.data.influence_distance)
|
|
||||||
elif ob.type == 'CAMERA':
|
|
||||||
vertex_pos = bbox_from_obj(ob, ob.data.display_size)
|
|
||||||
elif hasattr(ob, 'bound_box'):
|
|
||||||
vertex_indices = (
|
|
||||||
(0, 1), (1, 2), (2, 3), (0, 3),
|
|
||||||
(4, 5), (5, 6), (6, 7), (4, 7),
|
|
||||||
(0, 4), (1, 5), (2, 6), (3, 7))
|
|
||||||
vertex_pos = get_bb_coords_from_obj(ob)
|
|
||||||
|
|
||||||
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
|
|
||||||
batch = batch_for_shader(
|
|
||||||
shader,
|
|
||||||
'LINES',
|
|
||||||
{"pos": vertex_pos},
|
|
||||||
indices=vertex_indices)
|
|
||||||
|
|
||||||
shader.bind()
|
|
||||||
shader.uniform_float("color", self.data.get('color'))
|
|
||||||
batch.draw(shader)
|
|
||||||
|
|
||||||
|
shader.bind()
|
||||||
|
shader.uniform_float("color", self.data.get('color'))
|
||||||
|
batch.draw(shader)
|
||||||
|
|
||||||
class UserNameWidget(Widget):
|
class UserNameWidget(Widget):
|
||||||
draw_type = 'POST_PIXEL'
|
draw_type = 'POST_PIXEL'
|
||||||
@ -381,6 +425,62 @@ class UserNameWidget(Widget):
|
|||||||
blf.color(0, color[0], color[1], color[2], color[3])
|
blf.color(0, color[0], color[1], color[2], color[3])
|
||||||
blf.draw(0, self.username)
|
blf.draw(0, self.username)
|
||||||
|
|
||||||
|
class UserModeWidget(Widget):
|
||||||
|
draw_type = 'POST_PIXEL'
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
username):
|
||||||
|
self.username = username
|
||||||
|
self.settings = bpy.context.window_manager.session
|
||||||
|
self.preferences = get_preferences()
|
||||||
|
|
||||||
|
@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')
|
||||||
|
mode_current = self.data.get('mode_current')
|
||||||
|
user_selection = self.data.get('selected_objects')
|
||||||
|
|
||||||
|
return (scene_current == bpy.context.scene.name or
|
||||||
|
mode_current == bpy.context.mode or
|
||||||
|
self.settings.presence_show_far_user) and \
|
||||||
|
user_selection and \
|
||||||
|
self.settings.presence_show_mode and \
|
||||||
|
self.settings.enable_presence
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
user_selection = self.data.get('selected_objects')
|
||||||
|
area, region, rv3d = view3d_find()
|
||||||
|
viewport_coord = project_to_viewport(region, rv3d, (0, 0))
|
||||||
|
|
||||||
|
obj = find_from_attr("uuid", user_selection[0], bpy.data.objects)
|
||||||
|
if not obj:
|
||||||
|
return
|
||||||
|
mode_current = self.data.get('mode_current')
|
||||||
|
color = self.data.get('color')
|
||||||
|
origin_coord = project_to_screen(obj.location)
|
||||||
|
|
||||||
|
distance_viewport_object = math.sqrt((viewport_coord[0]-obj.location[0])**2+(viewport_coord[1]-obj.location[1])**2+(viewport_coord[2]-obj.location[2])**2)
|
||||||
|
|
||||||
|
if distance_viewport_object > self.preferences.presence_mode_distance :
|
||||||
|
return
|
||||||
|
|
||||||
|
if origin_coord :
|
||||||
|
blf.position(0, origin_coord[0]+8, origin_coord[1]-15, 0)
|
||||||
|
blf.size(0, 16, 72)
|
||||||
|
blf.color(0, color[0], color[1], color[2], color[3])
|
||||||
|
blf.draw(0, mode_current)
|
||||||
|
|
||||||
|
|
||||||
class SessionStatusWidget(Widget):
|
class SessionStatusWidget(Widget):
|
||||||
draw_type = 'POST_PIXEL'
|
draw_type = 'POST_PIXEL'
|
||||||
@ -463,6 +563,7 @@ class DrawFactory(object):
|
|||||||
try:
|
try:
|
||||||
for widget in self.widgets.values():
|
for widget in self.widgets.values():
|
||||||
if widget.draw_type == 'POST_VIEW' and widget.poll():
|
if widget.draw_type == 'POST_VIEW' and widget.poll():
|
||||||
|
widget.configure_bgl()
|
||||||
widget.draw()
|
widget.draw()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(
|
logging.error(
|
||||||
@ -472,6 +573,7 @@ class DrawFactory(object):
|
|||||||
try:
|
try:
|
||||||
for widget in self.widgets.values():
|
for widget in self.widgets.values():
|
||||||
if widget.draw_type == 'POST_PIXEL' and widget.poll():
|
if widget.draw_type == 'POST_PIXEL' and widget.poll():
|
||||||
|
widget.configure_bgl()
|
||||||
widget.draw()
|
widget.draw()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(
|
logging.error(
|
||||||
@ -484,6 +586,7 @@ this.renderer = DrawFactory()
|
|||||||
|
|
||||||
def register():
|
def register():
|
||||||
this.renderer.register_handlers()
|
this.renderer.register_handlers()
|
||||||
|
|
||||||
|
|
||||||
this.renderer.add_widget("session_status", SessionStatusWidget())
|
this.renderer.add_widget("session_status", SessionStatusWidget())
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ from replication.interface import session
|
|||||||
from replication import porcelain
|
from replication import porcelain
|
||||||
|
|
||||||
from . import operators, utils
|
from . import operators, utils
|
||||||
from .presence import (UserFrustumWidget, UserNameWidget, UserSelectionWidget,
|
from .presence import (UserFrustumWidget, UserNameWidget, UserModeWidget, UserSelectionWidget,
|
||||||
generate_user_camera, get_view_matrix, refresh_3d_view,
|
generate_user_camera, get_view_matrix, refresh_3d_view,
|
||||||
refresh_sidebar_view, renderer)
|
refresh_sidebar_view, renderer)
|
||||||
|
|
||||||
@ -41,7 +41,8 @@ this.registry = dict()
|
|||||||
def is_annotating(context: bpy.types.Context):
|
def is_annotating(context: bpy.types.Context):
|
||||||
""" Check if the annotate mode is enabled
|
""" Check if the annotate mode is enabled
|
||||||
"""
|
"""
|
||||||
return bpy.context.workspace.tools.from_space_view3d_mode('OBJECT', create=False).idname == 'builtin.annotate'
|
active_tool = bpy.context.workspace.tools.from_space_view3d_mode('OBJECT', create=False)
|
||||||
|
return (active_tool and active_tool.idname == 'builtin.annotate')
|
||||||
|
|
||||||
|
|
||||||
class Timer(object):
|
class Timer(object):
|
||||||
@ -136,12 +137,51 @@ class ApplyTimer(Timer):
|
|||||||
force=True)
|
force=True)
|
||||||
|
|
||||||
|
|
||||||
|
class AnnotationUpdates(Timer):
|
||||||
|
def __init__(self, timeout=1):
|
||||||
|
self._annotating = False
|
||||||
|
self._settings = utils.get_preferences()
|
||||||
|
|
||||||
|
super().__init__(timeout)
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
if session and session.state == STATE_ACTIVE:
|
||||||
|
ctx = bpy.context
|
||||||
|
annotation_gp = ctx.scene.grease_pencil
|
||||||
|
|
||||||
|
if annotation_gp and not annotation_gp.uuid:
|
||||||
|
ctx.scene.update_tag()
|
||||||
|
|
||||||
|
# if an annotation exist and is tracked
|
||||||
|
if annotation_gp and annotation_gp.uuid:
|
||||||
|
registered_gp = session.repository.graph.get(annotation_gp.uuid)
|
||||||
|
if is_annotating(bpy.context):
|
||||||
|
# try to get the right on it
|
||||||
|
if registered_gp.owner == RP_COMMON:
|
||||||
|
self._annotating = True
|
||||||
|
logging.debug(
|
||||||
|
"Getting the right on the annotation GP")
|
||||||
|
porcelain.lock(session.repository,
|
||||||
|
[registered_gp.uuid],
|
||||||
|
ignore_warnings=True,
|
||||||
|
affect_dependencies=False)
|
||||||
|
|
||||||
|
if registered_gp.owner == self._settings.username:
|
||||||
|
porcelain.commit(session.repository, annotation_gp.uuid)
|
||||||
|
porcelain.push(session.repository, 'origin', annotation_gp.uuid)
|
||||||
|
|
||||||
|
elif self._annotating:
|
||||||
|
porcelain.unlock(session.repository,
|
||||||
|
[registered_gp.uuid],
|
||||||
|
ignore_warnings=True,
|
||||||
|
affect_dependencies=False)
|
||||||
|
self._annotating = False
|
||||||
|
|
||||||
class DynamicRightSelectTimer(Timer):
|
class DynamicRightSelectTimer(Timer):
|
||||||
def __init__(self, timeout=.1):
|
def __init__(self, timeout=.1):
|
||||||
super().__init__(timeout)
|
super().__init__(timeout)
|
||||||
self._last_selection = []
|
self._last_selection = set()
|
||||||
self._user = None
|
self._user = None
|
||||||
self._annotating = False
|
|
||||||
|
|
||||||
def execute(self):
|
def execute(self):
|
||||||
settings = utils.get_preferences()
|
settings = utils.get_preferences()
|
||||||
@ -152,83 +192,46 @@ class DynamicRightSelectTimer(Timer):
|
|||||||
self._user = session.online_users.get(settings.username)
|
self._user = session.online_users.get(settings.username)
|
||||||
|
|
||||||
if self._user:
|
if self._user:
|
||||||
ctx = bpy.context
|
current_selection = set(utils.get_selected_objects(
|
||||||
annotation_gp = ctx.scene.grease_pencil
|
|
||||||
|
|
||||||
if annotation_gp and not annotation_gp.uuid:
|
|
||||||
ctx.scene.update_tag()
|
|
||||||
|
|
||||||
# if an annotation exist and is tracked
|
|
||||||
if annotation_gp and annotation_gp.uuid:
|
|
||||||
registered_gp = session.repository.graph.get(annotation_gp.uuid)
|
|
||||||
if is_annotating(bpy.context):
|
|
||||||
# try to get the right on it
|
|
||||||
if registered_gp.owner == RP_COMMON:
|
|
||||||
self._annotating = True
|
|
||||||
logging.debug(
|
|
||||||
"Getting the right on the annotation GP")
|
|
||||||
porcelain.lock(session.repository,
|
|
||||||
registered_gp.uuid,
|
|
||||||
ignore_warnings=True,
|
|
||||||
affect_dependencies=False)
|
|
||||||
|
|
||||||
if registered_gp.owner == settings.username:
|
|
||||||
gp_node = session.repository.graph.get(annotation_gp.uuid)
|
|
||||||
porcelain.commit(session.repository, gp_node.uuid)
|
|
||||||
porcelain.push(session.repository, 'origin', gp_node.uuid)
|
|
||||||
|
|
||||||
elif self._annotating:
|
|
||||||
porcelain.unlock(session.repository,
|
|
||||||
registered_gp.uuid,
|
|
||||||
ignore_warnings=True,
|
|
||||||
affect_dependencies=False)
|
|
||||||
|
|
||||||
current_selection = utils.get_selected_objects(
|
|
||||||
bpy.context.scene,
|
bpy.context.scene,
|
||||||
bpy.data.window_managers['WinMan'].windows[0].view_layer
|
bpy.data.window_managers['WinMan'].windows[0].view_layer
|
||||||
)
|
))
|
||||||
if current_selection != self._last_selection:
|
if current_selection != self._last_selection:
|
||||||
obj_common = [
|
to_lock = list(current_selection.difference(self._last_selection))
|
||||||
o for o in self._last_selection if o not in current_selection]
|
to_release = list(self._last_selection.difference(current_selection))
|
||||||
obj_ours = [
|
instances_to_lock = list()
|
||||||
o for o in current_selection if o not in self._last_selection]
|
|
||||||
|
|
||||||
# change old selection right to common
|
for node_id in to_lock:
|
||||||
for obj in obj_common:
|
node = session.repository.graph.get(node_id)
|
||||||
node = session.repository.graph.get(obj)
|
instance_mode = node.data.get('instance_type')
|
||||||
|
if instance_mode and instance_mode == 'COLLECTION':
|
||||||
|
to_lock.remove(node_id)
|
||||||
|
instances_to_lock.append(node_id)
|
||||||
|
if instances_to_lock:
|
||||||
|
try:
|
||||||
|
porcelain.lock(session.repository,
|
||||||
|
instances_to_lock,
|
||||||
|
ignore_warnings=True,
|
||||||
|
affect_dependencies=False)
|
||||||
|
except NonAuthorizedOperationError as e:
|
||||||
|
logging.warning(e)
|
||||||
|
|
||||||
if node and (node.owner == settings.username or node.owner == RP_COMMON):
|
if to_release:
|
||||||
recursive = True
|
try:
|
||||||
if node.data and 'instance_type' in node.data.keys():
|
porcelain.unlock(session.repository,
|
||||||
recursive = node.data['instance_type'] != 'COLLECTION'
|
to_release,
|
||||||
try:
|
ignore_warnings=True,
|
||||||
porcelain.unlock(session.repository,
|
affect_dependencies=True)
|
||||||
node.uuid,
|
except NonAuthorizedOperationError as e:
|
||||||
ignore_warnings=True,
|
logging.warning(e)
|
||||||
affect_dependencies=recursive)
|
if to_lock:
|
||||||
except NonAuthorizedOperationError:
|
try:
|
||||||
logging.warning(
|
porcelain.lock(session.repository,
|
||||||
f"Not authorized to change {node} owner")
|
to_lock,
|
||||||
|
ignore_warnings=True,
|
||||||
# change new selection to our
|
affect_dependencies=True)
|
||||||
for obj in obj_ours:
|
except NonAuthorizedOperationError as e:
|
||||||
node = session.repository.graph.get(obj)
|
logging.warning(e)
|
||||||
|
|
||||||
if node and node.owner == RP_COMMON:
|
|
||||||
recursive = True
|
|
||||||
if node.data and 'instance_type' in node.data.keys():
|
|
||||||
recursive = node.data['instance_type'] != 'COLLECTION'
|
|
||||||
|
|
||||||
try:
|
|
||||||
porcelain.lock(session.repository,
|
|
||||||
node.uuid,
|
|
||||||
ignore_warnings=True,
|
|
||||||
affect_dependencies=recursive)
|
|
||||||
except NonAuthorizedOperationError:
|
|
||||||
logging.warning(
|
|
||||||
f"Not authorized to change {node} owner")
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._last_selection = current_selection
|
self._last_selection = current_selection
|
||||||
|
|
||||||
@ -242,17 +245,16 @@ class DynamicRightSelectTimer(Timer):
|
|||||||
# Fix deselection until right managment refactoring (with Roles concepts)
|
# Fix deselection until right managment refactoring (with Roles concepts)
|
||||||
if len(current_selection) == 0 :
|
if len(current_selection) == 0 :
|
||||||
owned_keys = [k for k, v in session.repository.graph.items() if v.owner==settings.username]
|
owned_keys = [k for k, v in session.repository.graph.items() if v.owner==settings.username]
|
||||||
for key in owned_keys:
|
if owned_keys:
|
||||||
node = session.repository.graph.get(key)
|
|
||||||
try:
|
try:
|
||||||
porcelain.unlock(session.repository,
|
porcelain.unlock(session.repository,
|
||||||
key,
|
owned_keys,
|
||||||
ignore_warnings=True,
|
ignore_warnings=True,
|
||||||
affect_dependencies=True)
|
affect_dependencies=True)
|
||||||
except NonAuthorizedOperationError:
|
except NonAuthorizedOperationError as e:
|
||||||
logging.warning(
|
logging.warning(e)
|
||||||
f"Not authorized to change {key} owner")
|
|
||||||
|
|
||||||
|
# Objects selectability
|
||||||
for obj in bpy.data.objects:
|
for obj in bpy.data.objects:
|
||||||
object_uuid = getattr(obj, 'uuid', None)
|
object_uuid = getattr(obj, 'uuid', None)
|
||||||
if object_uuid:
|
if object_uuid:
|
||||||
@ -309,7 +311,8 @@ class ClientUpdate(Timer):
|
|||||||
settings.client_color.b,
|
settings.client_color.b,
|
||||||
1),
|
1),
|
||||||
'frame_current': bpy.context.scene.frame_current,
|
'frame_current': bpy.context.scene.frame_current,
|
||||||
'scene_current': scene_current
|
'scene_current': scene_current,
|
||||||
|
'mode_current': bpy.context.mode
|
||||||
}
|
}
|
||||||
porcelain.update_user_metadata(session.repository, metadata)
|
porcelain.update_user_metadata(session.repository, metadata)
|
||||||
|
|
||||||
@ -323,6 +326,9 @@ class ClientUpdate(Timer):
|
|||||||
local_user_metadata['view_matrix'] = get_view_matrix(
|
local_user_metadata['view_matrix'] = get_view_matrix(
|
||||||
)
|
)
|
||||||
porcelain.update_user_metadata(session.repository, local_user_metadata)
|
porcelain.update_user_metadata(session.repository, local_user_metadata)
|
||||||
|
elif bpy.context.mode != local_user_metadata['mode_current']:
|
||||||
|
local_user_metadata['mode_current'] = bpy.context.mode
|
||||||
|
porcelain.update_user_metadata(session.repository, local_user_metadata)
|
||||||
|
|
||||||
|
|
||||||
class SessionStatusUpdate(Timer):
|
class SessionStatusUpdate(Timer):
|
||||||
@ -350,6 +356,7 @@ class SessionUserSync(Timer):
|
|||||||
renderer.remove_widget(f"{user.username}_cam")
|
renderer.remove_widget(f"{user.username}_cam")
|
||||||
renderer.remove_widget(f"{user.username}_select")
|
renderer.remove_widget(f"{user.username}_select")
|
||||||
renderer.remove_widget(f"{user.username}_name")
|
renderer.remove_widget(f"{user.username}_name")
|
||||||
|
renderer.remove_widget(f"{user.username}_mode")
|
||||||
ui_users.remove(index)
|
ui_users.remove(index)
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -365,6 +372,8 @@ class SessionUserSync(Timer):
|
|||||||
f"{user}_select", UserSelectionWidget(user))
|
f"{user}_select", UserSelectionWidget(user))
|
||||||
renderer.add_widget(
|
renderer.add_widget(
|
||||||
f"{user}_name", UserNameWidget(user))
|
f"{user}_name", UserNameWidget(user))
|
||||||
|
renderer.add_widget(
|
||||||
|
f"{user}_mode", UserModeWidget(user))
|
||||||
|
|
||||||
|
|
||||||
class MainThreadExecutor(Timer):
|
class MainThreadExecutor(Timer):
|
||||||
|
531
multi_user/ui.py
@ -16,7 +16,9 @@
|
|||||||
# ##### END GPL LICENSE BLOCK #####
|
# ##### END GPL LICENSE BLOCK #####
|
||||||
|
|
||||||
|
|
||||||
|
from logging import log
|
||||||
import bpy
|
import bpy
|
||||||
|
import bpy.utils.previews
|
||||||
|
|
||||||
from .utils import get_preferences, get_expanded_icon, get_folder_size, get_state_str
|
from .utils import get_preferences, get_expanded_icon, get_folder_size, get_state_str
|
||||||
from replication.constants import (ADDED, ERROR, FETCHED,
|
from replication.constants import (ADDED, ERROR, FETCHED,
|
||||||
@ -71,162 +73,132 @@ class SESSION_PT_settings(bpy.types.Panel):
|
|||||||
|
|
||||||
def draw_header(self, context):
|
def draw_header(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
settings = get_preferences()
|
||||||
|
|
||||||
|
from multi_user import icons
|
||||||
|
offline_icon = icons.icons_col["session_status_offline"]
|
||||||
|
waiting_icon = icons.icons_col["session_status_waiting"]
|
||||||
|
online_icon = icons.icons_col["session_status_online"]
|
||||||
|
|
||||||
if session and session.state != STATE_INITIAL:
|
if session and session.state != STATE_INITIAL:
|
||||||
cli_state = session.state
|
cli_state = session.state
|
||||||
state = session.state
|
state = session.state
|
||||||
connection_icon = "KEYTYPE_MOVING_HOLD_VEC"
|
connection_icon = offline_icon
|
||||||
|
|
||||||
if state == STATE_ACTIVE:
|
if state == STATE_ACTIVE:
|
||||||
connection_icon = 'PROP_ON'
|
connection_icon = online_icon
|
||||||
else:
|
else:
|
||||||
connection_icon = 'PROP_CON'
|
connection_icon = waiting_icon
|
||||||
|
|
||||||
layout.label(text=f"Session - {get_state_str(cli_state)}", icon=connection_icon)
|
layout.label(text=f"{str(settings.server_name)} - {get_state_str(cli_state)}", icon_value=connection_icon.icon_id)
|
||||||
else:
|
else:
|
||||||
layout.label(text=f"Session - v{__version__}",icon="PROP_OFF")
|
layout.label(text=f"Multi-user - v{__version__}", icon="ANTIALIASED")
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
row = layout.row()
|
|
||||||
runtime_settings = context.window_manager.session
|
runtime_settings = context.window_manager.session
|
||||||
settings = get_preferences()
|
settings = get_preferences()
|
||||||
|
|
||||||
if hasattr(context.window_manager, 'session'):
|
if settings.is_first_launch:
|
||||||
# STATE INITIAL
|
# USER SETTINGS
|
||||||
if not session \
|
row = layout.row()
|
||||||
or (session and session.state == STATE_INITIAL):
|
row.label(text="1. Enter your username and color:")
|
||||||
pass
|
row = layout.row()
|
||||||
else:
|
split = row.split(factor=0.7, align=True)
|
||||||
progress = session.state_progress
|
split.prop(settings, "username", text="")
|
||||||
row = layout.row()
|
split.prop(settings, "client_color", text="")
|
||||||
|
|
||||||
current_state = session.state
|
# DOC
|
||||||
info_msg = None
|
row = layout.row()
|
||||||
|
row.label(text="2. New here ? See the doc:")
|
||||||
|
row = layout.row()
|
||||||
|
row.operator("doc.get", text="Documentation", icon="HELP")
|
||||||
|
|
||||||
|
# START
|
||||||
|
row = layout.row()
|
||||||
|
row.label(text="3: Start the Multi-user:")
|
||||||
|
row = layout.row()
|
||||||
|
row.scale_y = 2
|
||||||
|
row.operator("firstlaunch.verify", text="Continue")
|
||||||
|
|
||||||
|
if not settings.is_first_launch:
|
||||||
|
if hasattr(context.window_manager, 'session'):
|
||||||
|
# STATE INITIAL
|
||||||
|
if not session \
|
||||||
|
or (session and session.state == STATE_INITIAL):
|
||||||
|
layout = self.layout
|
||||||
|
settings = get_preferences()
|
||||||
|
server_preset = settings.server_preset
|
||||||
|
selected_server = context.window_manager.server_index if context.window_manager.server_index<=len(server_preset)-1 else 0
|
||||||
|
active_server_name = server_preset[selected_server].name if len(server_preset)>=1 else ""
|
||||||
|
is_server_selected = True if active_server_name else False
|
||||||
|
|
||||||
if current_state in [STATE_ACTIVE]:
|
# SERVER LIST
|
||||||
row = row.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=True)
|
row = layout.row()
|
||||||
row.prop(settings.sync_flags, "sync_render_settings",text="",icon_only=True, icon='SCENE')
|
box = row.box()
|
||||||
row.prop(settings.sync_flags, "sync_during_editmode", text="",icon_only=True, icon='EDITMODE_HLT')
|
box.scale_y = 0.7
|
||||||
row.prop(settings.sync_flags, "sync_active_camera", text="",icon_only=True, icon='OBJECT_DATAMODE')
|
split = box.split(factor=0.7)
|
||||||
|
split.label(text="Server")
|
||||||
|
split.label(text="Online")
|
||||||
|
|
||||||
row= layout.row()
|
col = row.column(align=True)
|
||||||
|
col.operator("session.get_info", icon="FILE_REFRESH", text="")
|
||||||
|
|
||||||
if current_state in [STATE_ACTIVE] and runtime_settings.is_host:
|
row = layout.row()
|
||||||
info_msg = f"LAN: {runtime_settings.internet_ip}"
|
col = row.column(align=True)
|
||||||
if current_state == STATE_LOBBY:
|
col.template_list("SESSION_UL_network", "", settings, "server_preset", context.window_manager, "server_index")
|
||||||
info_msg = "Waiting for the session to start."
|
col.separator()
|
||||||
|
connectOp = col.row()
|
||||||
|
connectOp.enabled =is_server_selected
|
||||||
|
connectOp.operator("session.connect", text="Connect")
|
||||||
|
|
||||||
if info_msg:
|
col = row.column(align=True)
|
||||||
info_box = row.box()
|
col.operator("session.preset_server_add", icon="ADD", text="") # TODO : add conditions (need a name, etc..)
|
||||||
info_box.row().label(text=info_msg,icon='INFO')
|
row_visible = col.row(align=True)
|
||||||
|
col_visible = row_visible.column(align=True)
|
||||||
|
col_visible.enabled = is_server_selected
|
||||||
|
col_visible.operator("session.preset_server_remove", icon="REMOVE", text="").target_server_name = active_server_name
|
||||||
|
col_visible.separator()
|
||||||
|
col_visible.operator("session.preset_server_edit", icon="GREASEPENCIL", text="").target_server_name = active_server_name
|
||||||
|
|
||||||
# Progress bar
|
else:
|
||||||
if current_state in [STATE_SYNCING, STATE_SRV_SYNC, STATE_WAITING]:
|
exitbutton = layout.row()
|
||||||
info_box = row.box()
|
exitbutton.scale_y = 1.5
|
||||||
info_box.row().label(text=printProgressBar(
|
exitbutton.operator("session.stop", icon='QUIT', text="Disconnect")
|
||||||
progress['current'],
|
|
||||||
progress['total'],
|
|
||||||
length=16
|
|
||||||
))
|
|
||||||
|
|
||||||
layout.row().operator("session.stop", icon='QUIT', text="Exit")
|
progress = session.state_progress
|
||||||
|
current_state = session.state
|
||||||
|
info_msg = None
|
||||||
|
|
||||||
|
if current_state == STATE_LOBBY:
|
||||||
|
usr = session.online_users.get(settings.username)
|
||||||
|
row= layout.row()
|
||||||
|
info_msg = "Waiting for the session to start."
|
||||||
|
if usr and usr['admin']:
|
||||||
|
info_msg = "Init the session to start."
|
||||||
|
info_box = layout.row()
|
||||||
|
info_box.label(text=info_msg,icon='INFO')
|
||||||
|
init_row = layout.row()
|
||||||
|
init_row.operator("session.init", icon='TOOL_SETTINGS', text="Init")
|
||||||
|
else:
|
||||||
|
info_box = layout.row()
|
||||||
|
info_box.row().label(text=info_msg,icon='INFO')
|
||||||
|
|
||||||
class SESSION_PT_settings_network(bpy.types.Panel):
|
# PROGRESS BAR
|
||||||
bl_idname = "MULTIUSER_SETTINGS_NETWORK_PT_panel"
|
if current_state in [STATE_SYNCING, STATE_SRV_SYNC, STATE_WAITING]:
|
||||||
bl_label = "Network"
|
row= layout.row()
|
||||||
bl_space_type = 'VIEW_3D'
|
row.label(text=f"Status: {get_state_str(current_state)}")
|
||||||
bl_region_type = 'UI'
|
row= layout.row()
|
||||||
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
|
info_box = row.box()
|
||||||
|
info_box.label(text=printProgressBar(
|
||||||
|
progress['current'],
|
||||||
|
progress['total'],
|
||||||
|
length=16
|
||||||
|
))
|
||||||
|
|
||||||
@classmethod
|
class SESSION_PT_host_settings(bpy.types.Panel):
|
||||||
def poll(cls, context):
|
bl_idname = "MULTIUSER_SETTINGS_HOST_PT_panel"
|
||||||
return not session \
|
bl_label = "Hosting"
|
||||||
or (session and session.state == 0)
|
|
||||||
|
|
||||||
def draw_header(self, context):
|
|
||||||
self.layout.label(text="", icon='URL')
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
|
|
||||||
runtime_settings = context.window_manager.session
|
|
||||||
settings = get_preferences()
|
|
||||||
|
|
||||||
# USER SETTINGS
|
|
||||||
row = layout.row()
|
|
||||||
row.prop(runtime_settings, "session_mode", expand=True)
|
|
||||||
row = layout.row()
|
|
||||||
|
|
||||||
col = row.row(align=True)
|
|
||||||
col.prop(settings, "server_preset_interface", text="")
|
|
||||||
col.operator("session.preset_server_add", icon='ADD', text="")
|
|
||||||
col.operator("session.preset_server_remove", icon='REMOVE', text="")
|
|
||||||
|
|
||||||
row = layout.row()
|
|
||||||
box = row.box()
|
|
||||||
|
|
||||||
if runtime_settings.session_mode == 'HOST':
|
|
||||||
row = box.row()
|
|
||||||
row.label(text="Port:")
|
|
||||||
row.prop(settings, "port", text="")
|
|
||||||
row = box.row()
|
|
||||||
row.label(text="Start from:")
|
|
||||||
row.prop(settings, "init_method", text="")
|
|
||||||
row = box.row()
|
|
||||||
row.label(text="Admin password:")
|
|
||||||
row.prop(settings, "password", text="")
|
|
||||||
row = box.row()
|
|
||||||
row.operator("session.start", text="HOST").host = True
|
|
||||||
else:
|
|
||||||
row = box.row()
|
|
||||||
row.prop(settings, "ip", text="IP")
|
|
||||||
row = box.row()
|
|
||||||
row.label(text="Port:")
|
|
||||||
row.prop(settings, "port", text="")
|
|
||||||
|
|
||||||
row = box.row()
|
|
||||||
row.prop(runtime_settings, "admin", text='Connect as admin', icon='DISCLOSURE_TRI_DOWN' if runtime_settings.admin
|
|
||||||
else 'DISCLOSURE_TRI_RIGHT')
|
|
||||||
if runtime_settings.admin:
|
|
||||||
row = box.row()
|
|
||||||
row.label(text="Password:")
|
|
||||||
row.prop(settings, "password", text="")
|
|
||||||
row = box.row()
|
|
||||||
row.operator("session.start", text="CONNECT").host = False
|
|
||||||
|
|
||||||
class SESSION_PT_settings_user(bpy.types.Panel):
|
|
||||||
bl_idname = "MULTIUSER_SETTINGS_USER_PT_panel"
|
|
||||||
bl_label = "User info"
|
|
||||||
bl_space_type = 'VIEW_3D'
|
|
||||||
bl_region_type = 'UI'
|
|
||||||
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
return not session \
|
|
||||||
or (session and session.state == 0)
|
|
||||||
|
|
||||||
def draw_header(self, context):
|
|
||||||
self.layout.label(text="", icon='USER')
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
|
|
||||||
runtime_settings = context.window_manager.session
|
|
||||||
settings = get_preferences()
|
|
||||||
|
|
||||||
row = layout.row()
|
|
||||||
# USER SETTINGS
|
|
||||||
row.prop(settings, "username", text="name")
|
|
||||||
|
|
||||||
row = layout.row()
|
|
||||||
row.prop(settings, "client_color", text="color")
|
|
||||||
row = layout.row()
|
|
||||||
|
|
||||||
|
|
||||||
class SESSION_PT_advanced_settings(bpy.types.Panel):
|
|
||||||
bl_idname = "MULTIUSER_SETTINGS_REPLICATION_PT_panel"
|
|
||||||
bl_label = "Advanced"
|
|
||||||
bl_space_type = 'VIEW_3D'
|
bl_space_type = 'VIEW_3D'
|
||||||
bl_region_type = 'UI'
|
bl_region_type = 'UI'
|
||||||
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
|
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
|
||||||
@ -234,19 +206,68 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
|
settings = get_preferences()
|
||||||
return not session \
|
return not session \
|
||||||
or (session and session.state == 0)
|
or (session and session.state == 0) \
|
||||||
|
and not settings.sidebar_advanced_shown \
|
||||||
|
and not settings.is_first_launch
|
||||||
|
|
||||||
|
def draw_header(self, context):
|
||||||
|
self.layout.label(text="", icon='NETWORK_DRIVE')
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
settings = get_preferences()
|
||||||
|
|
||||||
|
#HOST
|
||||||
|
host_selection = layout.row().box()
|
||||||
|
host_selection_row = host_selection.row()
|
||||||
|
host_selection_row.label(text="Init the session from:")
|
||||||
|
host_selection_row.prop(settings, "init_method", text="")
|
||||||
|
host_selection_row = host_selection.row()
|
||||||
|
host_selection_row.label(text="Port:")
|
||||||
|
host_selection_row.prop(settings, "host_port", text="")
|
||||||
|
host_selection_row = host_selection.row()
|
||||||
|
host_selection_col = host_selection_row.column()
|
||||||
|
host_selection_col.prop(settings, "host_use_server_password", text="Server password:")
|
||||||
|
host_selection_col = host_selection_row.column()
|
||||||
|
host_selection_col.enabled = True if settings.host_use_server_password else False
|
||||||
|
host_selection_col.prop(settings, "host_server_password", text="")
|
||||||
|
host_selection_row = host_selection.row()
|
||||||
|
host_selection_col = host_selection_row.column()
|
||||||
|
host_selection_col.prop(settings, "host_use_admin_password", text="Admin password:")
|
||||||
|
host_selection_col = host_selection_row.column()
|
||||||
|
host_selection_col.enabled = True if settings.host_use_admin_password else False
|
||||||
|
host_selection_col.prop(settings, "host_admin_password", text="")
|
||||||
|
|
||||||
|
host_selection = layout.column()
|
||||||
|
host_selection.operator("session.host", text="Host")
|
||||||
|
|
||||||
|
|
||||||
|
class SESSION_PT_advanced_settings(bpy.types.Panel):
|
||||||
|
bl_idname = "MULTIUSER_SETTINGS_REPLICATION_PT_panel"
|
||||||
|
bl_label = "General Settings"
|
||||||
|
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):
|
||||||
|
settings = get_preferences()
|
||||||
|
return not session \
|
||||||
|
or (session and session.state == 0) \
|
||||||
|
and not settings.sidebar_advanced_shown \
|
||||||
|
and not settings.is_first_launch
|
||||||
|
|
||||||
def draw_header(self, context):
|
def draw_header(self, context):
|
||||||
self.layout.label(text="", icon='PREFERENCES')
|
self.layout.label(text="", icon='PREFERENCES')
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
|
||||||
runtime_settings = context.window_manager.session
|
|
||||||
settings = get_preferences()
|
settings = get_preferences()
|
||||||
|
|
||||||
|
#ADVANCED NET
|
||||||
net_section = layout.row().box()
|
net_section = layout.row().box()
|
||||||
net_section.prop(
|
net_section.prop(
|
||||||
settings,
|
settings,
|
||||||
@ -254,12 +275,15 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
|
|||||||
text="Network",
|
text="Network",
|
||||||
icon=get_expanded_icon(settings.sidebar_advanced_net_expanded),
|
icon=get_expanded_icon(settings.sidebar_advanced_net_expanded),
|
||||||
emboss=False)
|
emboss=False)
|
||||||
|
|
||||||
if settings.sidebar_advanced_net_expanded:
|
if settings.sidebar_advanced_net_expanded:
|
||||||
net_section_row = net_section.row()
|
net_section_row = net_section.row()
|
||||||
net_section_row.label(text="Timeout (ms):")
|
net_section_row.label(text="Timeout (ms):")
|
||||||
net_section_row.prop(settings, "connection_timeout", text="")
|
net_section_row.prop(settings, "connection_timeout", text="")
|
||||||
|
net_section_row = net_section.row()
|
||||||
|
net_section_row.label(text="Server ping (ms):")
|
||||||
|
net_section_row.prop(settings, "ping_timeout", text="")
|
||||||
|
|
||||||
|
#ADVANCED REPLICATION
|
||||||
replication_section = layout.row().box()
|
replication_section = layout.row().box()
|
||||||
replication_section.prop(
|
replication_section.prop(
|
||||||
settings,
|
settings,
|
||||||
@ -267,16 +291,12 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
|
|||||||
text="Replication",
|
text="Replication",
|
||||||
icon=get_expanded_icon(settings.sidebar_advanced_rep_expanded),
|
icon=get_expanded_icon(settings.sidebar_advanced_rep_expanded),
|
||||||
emboss=False)
|
emboss=False)
|
||||||
|
|
||||||
if settings.sidebar_advanced_rep_expanded:
|
if settings.sidebar_advanced_rep_expanded:
|
||||||
replication_section_row = replication_section.row()
|
|
||||||
|
|
||||||
replication_section_row = replication_section.row()
|
replication_section_row = replication_section.row()
|
||||||
replication_section_row.prop(settings.sync_flags, "sync_render_settings")
|
replication_section_row.prop(settings.sync_flags, "sync_render_settings")
|
||||||
replication_section_row = replication_section.row()
|
replication_section_row = replication_section.row()
|
||||||
replication_section_row.prop(settings.sync_flags, "sync_active_camera")
|
replication_section_row.prop(settings.sync_flags, "sync_active_camera")
|
||||||
replication_section_row = replication_section.row()
|
replication_section_row = replication_section.row()
|
||||||
|
|
||||||
replication_section_row.prop(settings.sync_flags, "sync_during_editmode")
|
replication_section_row.prop(settings.sync_flags, "sync_during_editmode")
|
||||||
replication_section_row = replication_section.row()
|
replication_section_row = replication_section.row()
|
||||||
if settings.sync_flags.sync_during_editmode:
|
if settings.sync_flags.sync_during_editmode:
|
||||||
@ -285,7 +305,7 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
|
|||||||
replication_section_row = replication_section.row()
|
replication_section_row = replication_section.row()
|
||||||
replication_section_row.prop(settings, "depsgraph_update_rate", text="Apply delay")
|
replication_section_row.prop(settings, "depsgraph_update_rate", text="Apply delay")
|
||||||
|
|
||||||
|
#ADVANCED CACHE
|
||||||
cache_section = layout.row().box()
|
cache_section = layout.row().box()
|
||||||
cache_section.prop(
|
cache_section.prop(
|
||||||
settings,
|
settings,
|
||||||
@ -303,6 +323,8 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
|
|||||||
cache_section_row.prop(settings, "clear_memory_filecache", text="")
|
cache_section_row.prop(settings, "clear_memory_filecache", text="")
|
||||||
cache_section_row = cache_section.row()
|
cache_section_row = cache_section.row()
|
||||||
cache_section_row.operator('session.clear_cache', text=f"Clear cache ({get_folder_size(settings.cache_directory)})")
|
cache_section_row.operator('session.clear_cache', text=f"Clear cache ({get_folder_size(settings.cache_directory)})")
|
||||||
|
|
||||||
|
#ADVANCED LOG
|
||||||
log_section = layout.row().box()
|
log_section = layout.row().box()
|
||||||
log_section.prop(
|
log_section.prop(
|
||||||
settings,
|
settings,
|
||||||
@ -310,11 +332,11 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
|
|||||||
text="Logging",
|
text="Logging",
|
||||||
icon=get_expanded_icon(settings.sidebar_advanced_log_expanded),
|
icon=get_expanded_icon(settings.sidebar_advanced_log_expanded),
|
||||||
emboss=False)
|
emboss=False)
|
||||||
|
|
||||||
if settings.sidebar_advanced_log_expanded:
|
if settings.sidebar_advanced_log_expanded:
|
||||||
log_section_row = log_section.row()
|
log_section_row = log_section.row()
|
||||||
log_section_row.label(text="Log level:")
|
log_section_row.label(text="Log level:")
|
||||||
log_section_row.prop(settings, 'logging_level', text="")
|
log_section_row.prop(settings, 'logging_level', text="")
|
||||||
|
|
||||||
class SESSION_PT_user(bpy.types.Panel):
|
class SESSION_PT_user(bpy.types.Panel):
|
||||||
bl_idname = "MULTIUSER_USER_PT_panel"
|
bl_idname = "MULTIUSER_USER_PT_panel"
|
||||||
bl_label = "Online users"
|
bl_label = "Online users"
|
||||||
@ -324,7 +346,8 @@ class SESSION_PT_user(bpy.types.Panel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return session and session.state in [STATE_ACTIVE, STATE_LOBBY]
|
return session \
|
||||||
|
and session.state in [STATE_ACTIVE, STATE_LOBBY]
|
||||||
|
|
||||||
def draw_header(self, context):
|
def draw_header(self, context):
|
||||||
self.layout.label(text="", icon='USER')
|
self.layout.label(text="", icon='USER')
|
||||||
@ -336,22 +359,23 @@ class SESSION_PT_user(bpy.types.Panel):
|
|||||||
settings = get_preferences()
|
settings = get_preferences()
|
||||||
active_user = online_users[selected_user] if len(
|
active_user = online_users[selected_user] if len(
|
||||||
online_users)-1 >= selected_user else 0
|
online_users)-1 >= selected_user else 0
|
||||||
runtime_settings = context.window_manager.session
|
|
||||||
|
|
||||||
# Create a simple row.
|
#USER LIST
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
box = row.box()
|
box = row.box()
|
||||||
split = box.split(factor=0.35)
|
split = box.split(factor=0.35)
|
||||||
split.label(text="user")
|
split.label(text="user")
|
||||||
split = split.split(factor=0.5)
|
split = split.split(factor=0.3)
|
||||||
split.label(text="location")
|
split.label(text="mode")
|
||||||
split.label(text="frame")
|
split.label(text="frame")
|
||||||
|
split.label(text="location")
|
||||||
split.label(text="ping")
|
split.label(text="ping")
|
||||||
|
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
layout.template_list("SESSION_UL_users", "", context.window_manager,
|
layout.template_list("SESSION_UL_users", "", context.window_manager,
|
||||||
"online_users", context.window_manager, "user_index")
|
"online_users", context.window_manager, "user_index")
|
||||||
|
|
||||||
|
#OPERATOR ON USER
|
||||||
if active_user != 0 and active_user.username != settings.username:
|
if active_user != 0 and active_user.username != settings.username:
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
user_operations = row.split()
|
user_operations = row.split()
|
||||||
@ -383,6 +407,8 @@ class SESSION_UL_users(bpy.types.UIList):
|
|||||||
ping = '-'
|
ping = '-'
|
||||||
frame_current = '-'
|
frame_current = '-'
|
||||||
scene_current = '-'
|
scene_current = '-'
|
||||||
|
mode_current = '-'
|
||||||
|
mode_icon = 'BLANK1'
|
||||||
status_icon = 'BLANK1'
|
status_icon = 'BLANK1'
|
||||||
if session:
|
if session:
|
||||||
user = session.online_users.get(item.username)
|
user = session.online_users.get(item.username)
|
||||||
@ -392,57 +418,49 @@ class SESSION_UL_users(bpy.types.UIList):
|
|||||||
if metadata and 'frame_current' in metadata:
|
if metadata and 'frame_current' in metadata:
|
||||||
frame_current = str(metadata.get('frame_current','-'))
|
frame_current = str(metadata.get('frame_current','-'))
|
||||||
scene_current = metadata.get('scene_current','-')
|
scene_current = metadata.get('scene_current','-')
|
||||||
|
mode_current = metadata.get('mode_current','-')
|
||||||
|
if mode_current == "OBJECT" :
|
||||||
|
mode_icon = "OBJECT_DATAMODE"
|
||||||
|
elif mode_current == "EDIT_MESH" :
|
||||||
|
mode_icon = "EDITMODE_HLT"
|
||||||
|
elif mode_current == 'EDIT_CURVE':
|
||||||
|
mode_icon = "CURVE_DATA"
|
||||||
|
elif mode_current == 'EDIT_SURFACE':
|
||||||
|
mode_icon = "SURFACE_DATA"
|
||||||
|
elif mode_current == 'EDIT_TEXT':
|
||||||
|
mode_icon = "FILE_FONT"
|
||||||
|
elif mode_current == 'EDIT_ARMATURE':
|
||||||
|
mode_icon = "ARMATURE_DATA"
|
||||||
|
elif mode_current == 'EDIT_METABALL':
|
||||||
|
mode_icon = "META_BALL"
|
||||||
|
elif mode_current == 'EDIT_LATTICE':
|
||||||
|
mode_icon = "LATTICE_DATA"
|
||||||
|
elif mode_current == 'POSE':
|
||||||
|
mode_icon = "POSE_HLT"
|
||||||
|
elif mode_current == 'SCULPT':
|
||||||
|
mode_icon = "SCULPTMODE_HLT"
|
||||||
|
elif mode_current == 'PAINT_WEIGHT':
|
||||||
|
mode_icon = "WPAINT_HLT"
|
||||||
|
elif mode_current == 'PAINT_VERTEX':
|
||||||
|
mode_icon = "VPAINT_HLT"
|
||||||
|
elif mode_current == 'PAINT_TEXTURE':
|
||||||
|
mode_icon = "TPAINT_HLT"
|
||||||
|
elif mode_current == 'PARTICLE':
|
||||||
|
mode_icon = "PARTICLES"
|
||||||
|
elif mode_current == 'PAINT_GPENCIL' or mode_current =='EDIT_GPENCIL' or mode_current =='SCULPT_GPENCIL' or mode_current =='WEIGHT_GPENCIL' or mode_current =='VERTEX_GPENCIL':
|
||||||
|
mode_icon = "GREASEPENCIL"
|
||||||
if user['admin']:
|
if user['admin']:
|
||||||
status_icon = 'FAKE_USER_ON'
|
status_icon = 'FAKE_USER_ON'
|
||||||
split = layout.split(factor=0.35)
|
split = layout.split(factor=0.35)
|
||||||
split.label(text=item.username, icon=status_icon)
|
split.label(text=item.username, icon=status_icon)
|
||||||
split = split.split(factor=0.5)
|
split = split.split(factor=0.3)
|
||||||
split.label(text=scene_current)
|
split.label(icon=mode_icon)
|
||||||
split.label(text=frame_current)
|
split.label(text=frame_current)
|
||||||
|
split.label(text=scene_current)
|
||||||
split.label(text=ping)
|
split.label(text=ping)
|
||||||
|
|
||||||
|
|
||||||
class SESSION_PT_presence(bpy.types.Panel):
|
|
||||||
bl_idname = "MULTIUSER_MODULE_PT_panel"
|
|
||||||
bl_label = "Presence overlay"
|
|
||||||
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 not session \
|
|
||||||
or (session and session.state in [STATE_INITIAL, STATE_ACTIVE])
|
|
||||||
|
|
||||||
def draw_header(self, context):
|
|
||||||
self.layout.prop(context.window_manager.session,
|
|
||||||
"enable_presence", text="",icon='OVERLAY')
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
|
|
||||||
settings = context.window_manager.session
|
|
||||||
pref = get_preferences()
|
|
||||||
layout.active = settings.enable_presence
|
|
||||||
col = layout.column()
|
|
||||||
col.prop(settings, "presence_show_session_status")
|
|
||||||
row = col.column()
|
|
||||||
row.active = settings.presence_show_session_status
|
|
||||||
row.prop(pref, "presence_hud_scale", expand=True)
|
|
||||||
row = col.column(align=True)
|
|
||||||
row.active = settings.presence_show_session_status
|
|
||||||
row.prop(pref, "presence_hud_hpos", expand=True)
|
|
||||||
row.prop(pref, "presence_hud_vpos", expand=True)
|
|
||||||
col.prop(settings, "presence_show_selected")
|
|
||||||
col.prop(settings, "presence_show_user")
|
|
||||||
row = layout.column()
|
|
||||||
row.active = settings.presence_show_user
|
|
||||||
row.prop(settings, "presence_show_far_user")
|
|
||||||
|
|
||||||
def draw_property(context, parent, property_uuid, level=0):
|
def draw_property(context, parent, property_uuid, level=0):
|
||||||
settings = get_preferences()
|
settings = get_preferences()
|
||||||
runtime_settings = context.window_manager.session
|
|
||||||
item = session.repository.graph.get(property_uuid)
|
item = session.repository.graph.get(property_uuid)
|
||||||
type_id = item.data.get('type_id')
|
type_id = item.data.get('type_id')
|
||||||
area_msg = parent.row(align=True)
|
area_msg = parent.row(align=True)
|
||||||
@ -462,15 +480,18 @@ def draw_property(context, parent, property_uuid, level=0):
|
|||||||
detail_item_box.label(text=f"{name}")
|
detail_item_box.label(text=f"{name}")
|
||||||
|
|
||||||
# Operations
|
# Operations
|
||||||
|
|
||||||
have_right_to_modify = (item.owner == settings.username or \
|
have_right_to_modify = (item.owner == settings.username or \
|
||||||
item.owner == RP_COMMON) and item.state != ERROR
|
item.owner == RP_COMMON) and item.state != ERROR
|
||||||
|
|
||||||
|
from multi_user import icons
|
||||||
|
sync_status = icons.icons_col["repository_push"] #TODO: Link all icons to the right sync (push/merge/issue). For issue use "UNLINKED" for icon
|
||||||
|
# sync_status = icons.icons_col["repository_merge"]
|
||||||
|
|
||||||
if have_right_to_modify:
|
if have_right_to_modify:
|
||||||
detail_item_box.operator(
|
detail_item_box.operator(
|
||||||
"session.commit",
|
"session.commit",
|
||||||
text="",
|
text="",
|
||||||
icon='TRIA_UP').target = item.uuid
|
icon_value=sync_status.icon_id).target = item.uuid
|
||||||
detail_item_box.separator()
|
detail_item_box.separator()
|
||||||
|
|
||||||
if item.state in [FETCHED, UP]:
|
if item.state in [FETCHED, UP]:
|
||||||
@ -502,12 +523,40 @@ def draw_property(context, parent, property_uuid, level=0):
|
|||||||
else:
|
else:
|
||||||
detail_item_box.label(text="", icon="DECORATE_LOCKED")
|
detail_item_box.label(text="", icon="DECORATE_LOCKED")
|
||||||
|
|
||||||
|
class SESSION_PT_sync(bpy.types.Panel):
|
||||||
|
bl_idname = "MULTIUSER_SYNC_PT_panel"
|
||||||
|
bl_label = "Synchronize"
|
||||||
|
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 session \
|
||||||
|
and session.state in [STATE_ACTIVE]
|
||||||
|
|
||||||
|
def draw_header(self, context):
|
||||||
|
self.layout.label(text="", icon='UV_SYNC_SELECT')
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
settings = get_preferences()
|
||||||
|
|
||||||
|
row= layout.row()
|
||||||
|
row = row.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=True)
|
||||||
|
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='VIEW_CAMERA')
|
||||||
|
|
||||||
|
|
||||||
class SESSION_PT_repository(bpy.types.Panel):
|
class SESSION_PT_repository(bpy.types.Panel):
|
||||||
bl_idname = "MULTIUSER_PROPERTIES_PT_panel"
|
bl_idname = "MULTIUSER_PROPERTIES_PT_panel"
|
||||||
bl_label = "Repository"
|
bl_label = "Repository"
|
||||||
bl_space_type = 'VIEW_3D'
|
bl_space_type = 'VIEW_3D'
|
||||||
bl_region_type = 'UI'
|
bl_region_type = 'UI'
|
||||||
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
|
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
|
||||||
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
@ -520,8 +569,8 @@ class SESSION_PT_repository(bpy.types.Panel):
|
|||||||
admin = usr['admin']
|
admin = usr['admin']
|
||||||
return hasattr(context.window_manager, 'session') and \
|
return hasattr(context.window_manager, 'session') and \
|
||||||
session and \
|
session and \
|
||||||
(session.state == STATE_ACTIVE or \
|
session.state == STATE_ACTIVE and \
|
||||||
session.state == STATE_LOBBY and admin)
|
not settings.sidebar_repository_shown
|
||||||
|
|
||||||
def draw_header(self, context):
|
def draw_header(self, context):
|
||||||
self.layout.label(text="", icon='OUTLINER_OB_GROUP_INSTANCE')
|
self.layout.label(text="", icon='OUTLINER_OB_GROUP_INSTANCE')
|
||||||
@ -535,19 +584,18 @@ class SESSION_PT_repository(bpy.types.Panel):
|
|||||||
|
|
||||||
usr = session.online_users.get(settings.username)
|
usr = session.online_users.get(settings.username)
|
||||||
|
|
||||||
row = layout.row()
|
|
||||||
|
|
||||||
if session.state == STATE_ACTIVE:
|
if session.state == STATE_ACTIVE:
|
||||||
if 'SessionBackupTimer' in registry:
|
if 'SessionBackupTimer' in registry:
|
||||||
|
row = layout.row()
|
||||||
row.alert = True
|
row.alert = True
|
||||||
row.operator('session.cancel_autosave', icon="CANCEL")
|
row.operator('session.cancel_autosave', icon="CANCEL")
|
||||||
row.alert = False
|
row.alert = False
|
||||||
else:
|
# else:
|
||||||
row.operator('session.save', icon="FILE_TICK")
|
# row.operator('session.save', icon="FILE_TICK")
|
||||||
|
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
row = box.row()
|
row = box.row()
|
||||||
row.prop(runtime_settings, "filter_owned", text="Show only owned Nodes", icon_only=True, icon="DECORATE_UNLOCKED")
|
row.prop(runtime_settings, "filter_owned", text="Only show owned data blocks", icon_only=True, icon="DECORATE_UNLOCKED")
|
||||||
row = box.row()
|
row = box.row()
|
||||||
row.prop(runtime_settings, "filter_name", text="Filter")
|
row.prop(runtime_settings, "filter_name", text="Filter")
|
||||||
row = box.row()
|
row = box.row()
|
||||||
@ -555,27 +603,18 @@ class SESSION_PT_repository(bpy.types.Panel):
|
|||||||
# Properties
|
# Properties
|
||||||
owned_nodes = [k for k, v in session.repository.graph.items() if v.owner==settings.username]
|
owned_nodes = [k for k, v in session.repository.graph.items() if v.owner==settings.username]
|
||||||
|
|
||||||
filtered_node = owned_nodes if runtime_settings.filter_owned else session.repository.graph.keys()
|
filtered_node = owned_nodes if runtime_settings.filter_owned else list(session.repository.graph.keys())
|
||||||
|
|
||||||
if runtime_settings.filter_name:
|
if runtime_settings.filter_name:
|
||||||
for node_id in filtered_node:
|
filtered_node = [n for n in filtered_node if runtime_settings.filter_name.lower() in session.repository.graph.get(n).data.get('name').lower()]
|
||||||
node_instance = session.repository.graph.get(node_id)
|
|
||||||
name = node_instance.data.get('name')
|
|
||||||
if runtime_settings.filter_name not in name:
|
|
||||||
filtered_node.remove(node_id)
|
|
||||||
|
|
||||||
if filtered_node:
|
if filtered_node:
|
||||||
col = layout.column(align=True)
|
col = layout.column(align=True)
|
||||||
for key in filtered_node:
|
for key in filtered_node:
|
||||||
draw_property(context, col, key)
|
draw_property(context, col, key)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
layout.row().label(text="Empty")
|
layout.row().label(text="Empty")
|
||||||
|
|
||||||
elif session.state == STATE_LOBBY and usr and usr['admin']:
|
|
||||||
row.operator("session.init", icon='TOOL_SETTINGS', text="Init")
|
|
||||||
else:
|
|
||||||
row.label(text="Waiting to start")
|
|
||||||
|
|
||||||
class VIEW3D_PT_overlay_session(bpy.types.Panel):
|
class VIEW3D_PT_overlay_session(bpy.types.Panel):
|
||||||
bl_space_type = 'VIEW_3D'
|
bl_space_type = 'VIEW_3D'
|
||||||
@ -590,37 +629,71 @@ class VIEW3D_PT_overlay_session(bpy.types.Panel):
|
|||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
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
|
settings = context.window_manager.session
|
||||||
|
pref = get_preferences()
|
||||||
layout.active = settings.enable_presence
|
layout.active = settings.enable_presence
|
||||||
col = layout.column()
|
|
||||||
col.prop(settings, "presence_show_session_status")
|
row = layout.row()
|
||||||
col.prop(settings, "presence_show_selected")
|
row.prop(settings, "presence_show_selected",text="Selected Objects")
|
||||||
col.prop(settings, "presence_show_user")
|
|
||||||
|
|
||||||
row = layout.column()
|
row = layout.row(align=True)
|
||||||
row.active = settings.presence_show_user
|
row.prop(settings, "presence_show_user", text="Users camera")
|
||||||
row.prop(settings, "presence_show_far_user")
|
row.prop(settings, "presence_show_mode", text="Users mode")
|
||||||
|
|
||||||
|
col = layout.column()
|
||||||
|
if settings.presence_show_mode or settings.presence_show_user:
|
||||||
|
row = col.column()
|
||||||
|
row.prop(pref, "presence_text_distance", expand=True)
|
||||||
|
|
||||||
|
row = col.column()
|
||||||
|
row.prop(settings, "presence_show_far_user", text="Users on different scenes")
|
||||||
|
|
||||||
|
col.prop(settings, "presence_show_session_status")
|
||||||
|
if settings.presence_show_session_status :
|
||||||
|
split = layout.split()
|
||||||
|
text_pos = split.column(align=True)
|
||||||
|
text_pos.active = settings.presence_show_session_status
|
||||||
|
text_pos.prop(pref, "presence_hud_hpos", expand=True)
|
||||||
|
text_pos.prop(pref, "presence_hud_vpos", expand=True)
|
||||||
|
text_scale = split.column()
|
||||||
|
text_scale.active = settings.presence_show_session_status
|
||||||
|
text_scale.prop(pref, "presence_hud_scale", expand=True)
|
||||||
|
|
||||||
|
|
||||||
|
class SESSION_UL_network(bpy.types.UIList):
|
||||||
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag):
|
||||||
|
settings = get_preferences()
|
||||||
|
server_name = '-'
|
||||||
|
server_status = 'BLANK1'
|
||||||
|
server_private = 'BLANK1'
|
||||||
|
|
||||||
|
server_name = item.server_name
|
||||||
|
|
||||||
|
split = layout.split(factor=0.7)
|
||||||
|
if item.is_private:
|
||||||
|
server_private = 'LOCKED'
|
||||||
|
split.label(text=server_name, icon=server_private)
|
||||||
|
else:
|
||||||
|
split.label(text=server_name)
|
||||||
|
|
||||||
|
from multi_user import icons
|
||||||
|
server_status = icons.icons_col["server_offline"]
|
||||||
|
if item.is_online:
|
||||||
|
server_status = icons.icons_col["server_online"]
|
||||||
|
split.label(icon_value=server_status.icon_id)
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
SESSION_UL_users,
|
SESSION_UL_users,
|
||||||
|
SESSION_UL_network,
|
||||||
SESSION_PT_settings,
|
SESSION_PT_settings,
|
||||||
SESSION_PT_settings_user,
|
SESSION_PT_host_settings,
|
||||||
SESSION_PT_settings_network,
|
|
||||||
SESSION_PT_presence,
|
|
||||||
SESSION_PT_advanced_settings,
|
SESSION_PT_advanced_settings,
|
||||||
SESSION_PT_user,
|
SESSION_PT_user,
|
||||||
|
SESSION_PT_sync,
|
||||||
SESSION_PT_repository,
|
SESSION_PT_repository,
|
||||||
VIEW3D_PT_overlay_session,
|
VIEW3D_PT_overlay_session,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
register, unregister = bpy.utils.register_classes_factory(classes)
|
register, unregister = bpy.utils.register_classes_factory(classes)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Download base image debian jessie
|
# Download base image debian jessie
|
||||||
FROM python:slim
|
FROM python:slim
|
||||||
|
|
||||||
ARG replication_version=0.1.13
|
ARG replication_version=0.9.1
|
||||||
ARG version=0.1.1
|
ARG version=0.1.1
|
||||||
|
|
||||||
# Infos
|
# Infos
|
||||||
@ -22,4 +22,4 @@ RUN pip install replication==$replication_version
|
|||||||
|
|
||||||
# Run the server with parameters
|
# Run the server with parameters
|
||||||
ENTRYPOINT ["/bin/sh", "-c"]
|
ENTRYPOINT ["/bin/sh", "-c"]
|
||||||
CMD ["python3 -m replication.server -pwd ${password} -p ${port} -t ${timeout} -l ${log_level} -lf ${log_file}"]
|
CMD ["replication.serve -apwd ${password} -spwd '' -p ${port} -t ${timeout} -l ${log_level} -lf ${log_file}"]
|
@ -12,6 +12,8 @@ def test_scene(clear_blend):
|
|||||||
get_preferences().sync_flags.sync_render_settings = True
|
get_preferences().sync_flags.sync_render_settings = True
|
||||||
|
|
||||||
datablock = bpy.data.scenes.new("toto")
|
datablock = bpy.data.scenes.new("toto")
|
||||||
|
datablock.timeline_markers.new('toto', frame=10)
|
||||||
|
datablock.timeline_markers.new('tata', frame=1)
|
||||||
datablock.view_settings.use_curve_mapping = True
|
datablock.view_settings.use_curve_mapping = True
|
||||||
# Test
|
# Test
|
||||||
implementation = BlScene()
|
implementation = BlScene()
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from deepdiff import DeepDiff
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
|
|
||||||
def test_start_session():
|
|
||||||
result = bpy.ops.session.start()
|
|
||||||
|
|
||||||
|
|
||||||
assert 'FINISHED' in result
|
|
||||||
|
|
||||||
def test_stop_session():
|
|
||||||
|
|
||||||
result = bpy.ops.session.stop()
|
|
||||||
|
|
||||||
assert 'FINISHED' in result
|
|