Compare commits
42 Commits
188-intgra
...
197-user-s
Author | SHA1 | Date | |
---|---|---|---|
3f005b86ab | |||
5098e5135d | |||
37cfed489c | |||
9003abcd18 | |||
a199e0df00 | |||
3774419b7e | |||
3e552cb406 | |||
9f381b44c8 | |||
ad795caed5 | |||
504dd77405 | |||
82022c9e4d | |||
d81b4dc014 | |||
63affa079f | |||
fcf5a12dd0 | |||
b0529e4444 | |||
bdfd89c085 | |||
ff1630f9cc | |||
5830fe1abb | |||
c609f72080 | |||
a28a6f91bd | |||
a996f39d3b | |||
7790a16034 | |||
836fdd02b8 | |||
7cb3482353 | |||
041022056c | |||
05f3eb1445 | |||
17193bde3a | |||
a14b4313f5 | |||
b203d9dffd | |||
f64db2155e | |||
e07ebdeff5 | |||
3d6453f7a2 | |||
7421511079 | |||
bc24525cec | |||
0c6491590e | |||
b87e733ddc | |||
cb0962b484 | |||
a1b6fb0533 | |||
b6a8a2ec01 | |||
3e41b18af1 | |||
f7c4f5d1fe | |||
a34f58ef3f |
18
README.md
18
README.md
@ -32,32 +32,32 @@ Currently, not all data-block are supported for replication over the wire. The f
|
|||||||
| Name | Status | Comment |
|
| Name | Status | Comment |
|
||||||
| -------------- | :----: | :----------------------------------------------------------: |
|
| -------------- | :----: | :----------------------------------------------------------: |
|
||||||
| action | ✔️ | |
|
| action | ✔️ | |
|
||||||
| armature | ❗ | Not stable |
|
|
||||||
| camera | ✔️ | |
|
| camera | ✔️ | |
|
||||||
| collection | ✔️ | |
|
| collection | ✔️ | |
|
||||||
| curve | ❗ | Nurbs surfaces not supported |
|
|
||||||
| 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 | ✔️ | |
|
||||||
| textures | ❗ | Supported for modifiers/materials/geo nodes only |
|
|
||||||
| texts | ✔️ | |
|
| texts | ✔️ | |
|
||||||
| scene | ✔️ | |
|
| scene | ✔️ | |
|
||||||
| world | ✔️ | |
|
| world | ✔️ | |
|
||||||
| lightprobes | ✔️ | |
|
|
||||||
| compositing | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/46) |
|
|
||||||
| texts | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/81) |
|
|
||||||
| nla | ❌ | |
|
|
||||||
| volumes | ✔️ | |
|
| volumes | ✔️ | |
|
||||||
|
| lightprobes | ✔️ | |
|
||||||
|
| physics | ✔️ | |
|
||||||
|
| curve | ❗ | Nurbs surfaces not supported |
|
||||||
|
| textures | ❗ | Supported for modifiers/materials/geo nodes only |
|
||||||
|
| armature | ❗ | Not stable |
|
||||||
| 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 |
|
||||||
| physics | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/45) |
|
|
||||||
| libraries | ❗ | Partial |
|
| libraries | ❗ | Partial |
|
||||||
|
| nla | ❌ | |
|
||||||
|
| texts | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/81) |
|
||||||
|
| compositing | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/46) |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
BIN
docs/getting_started/img/server_preset_exemple.gif
Normal file
BIN
docs/getting_started/img/server_preset_exemple.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 320 KiB |
BIN
docs/getting_started/img/server_preset_image_add.png
Normal file
BIN
docs/getting_started/img/server_preset_image_add.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.3 KiB |
BIN
docs/getting_started/img/server_preset_image_admin.png
Normal file
BIN
docs/getting_started/img/server_preset_image_admin.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
docs/getting_started/img/server_preset_image_normal_server.png
Normal file
BIN
docs/getting_started/img/server_preset_image_normal_server.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.0 KiB |
BIN
docs/getting_started/img/server_preset_image_report.png
Normal file
BIN
docs/getting_started/img/server_preset_image_report.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
@ -108,36 +108,69 @@ Before starting make sure that you have access to the session IP address and por
|
|||||||
1. Fill in your user information
|
1. Fill in your user information
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
||||||
Follow the user-info_ section for this step.
|
Joining a server
|
||||||
|
=======================
|
||||||
|
|
||||||
----------------
|
--------------
|
||||||
2. Network setup
|
Network setup
|
||||||
----------------
|
--------------
|
||||||
|
|
||||||
In the network panel, select **JOIN**.
|
In the network panel, select **JOIN**.
|
||||||
The **join sub-panel** (see image below) allows you to configure your client to join a
|
The **join sub-panel** (see image below) allows you to configure your client to join a
|
||||||
collaborative session which is already hosted.
|
collaborative session which is already hosted.
|
||||||
|
|
||||||
.. figure:: img/quickstart_join.png
|
.. figure:: img/server_preset_image_normal_server.png
|
||||||
:align: center
|
:align: center
|
||||||
:alt: Connect menu
|
:width: 200px
|
||||||
|
|
||||||
Connection panel
|
Connection pannel
|
||||||
|
|
||||||
Fill in the fields with your information:
|
Fill in the fields with your information:
|
||||||
|
|
||||||
- **IP**: the host's IP address.
|
- **IP**: the host's IP address.
|
||||||
- **Port**: the host's port number.
|
- **Port**: the host's port number.
|
||||||
- **Connect as admin**: connect yourself with **admin rights** (see :ref:`admin` ) to the session.
|
|
||||||
|
|
||||||
.. Maybe something more explicit here
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
Additional configuration settings can be found in the :ref:`advanced` section.
|
|
||||||
|
|
||||||
Once you've configured every field, hit the button **CONNECT** to join the session !
|
Once you've configured every field, hit the button **CONNECT** to join the session !
|
||||||
When the :ref:`session-status` is **ONLINE** you are online and ready to start co-creating.
|
When the :ref:`session-status` is **ONLINE** you are online and ready to start co-creating.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you want to have **administrator rights** (see :ref:`admin` ) on the server, just enter the password created by the host in the **Connect as admin** section
|
||||||
|
|
||||||
|
.. figure:: img/server_preset_image_admin.png
|
||||||
|
:align: center
|
||||||
|
:width: 200px
|
||||||
|
|
||||||
|
Admin password
|
||||||
|
|
||||||
|
---------------
|
||||||
|
Server presets
|
||||||
|
---------------
|
||||||
|
|
||||||
|
You can save your server presets in a preset list below the 'JOIN' and 'HOST' buttons. This allows you to quickly access and manage your servers.
|
||||||
|
|
||||||
|
To add a server, first enter the ip address and the port (plus the password if needed), then click on the + icon to add a name to your preset. To remove a server from the list, select it and click on the - icon.
|
||||||
|
|
||||||
|
.. figure:: img/server_preset_exemple.gif
|
||||||
|
:align: center
|
||||||
|
:width: 200px
|
||||||
|
|
||||||
|
.. warning:: Be careful, if you don't rename your new preset, or if it has the same name as an existing preset, the old preset will be overwritten.
|
||||||
|
|
||||||
|
.. figure:: img/server_preset_image_report.png
|
||||||
|
:align: center
|
||||||
|
:width: 200px
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Two presets are already present when the addon is launched:
|
||||||
|
|
||||||
|
- The 'localhost' preset, to host and join a local session quickly
|
||||||
|
- The 'public session' preset, to join the public sessions of the multi-user server (official discord to participate : https://discord.gg/aBPvGws)
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Additional configuration settings can be found in the :ref:`advanced` section.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
When starting a **dedicated server**, the session status screen will take you to the **LOBBY**, awaiting an admin to start the session.
|
When starting a **dedicated server**, the session status screen will take you to the **LOBBY**, awaiting an admin to start the session.
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ Hit 'Create a network'(see image below) and go to the network settings.
|
|||||||
:align: center
|
:align: center
|
||||||
:width: 450px
|
:width: 450px
|
||||||
|
|
||||||
Network page
|
Admin password
|
||||||
|
|
||||||
Now that the network is created, let's configure it.
|
Now that the network is created, let's configure it.
|
||||||
|
|
||||||
|
@ -43,9 +43,10 @@ from bpy.app.handlers import persistent
|
|||||||
from . import environment
|
from . import environment
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module_error_msg = "Insufficient rights to install the multi-user \
|
module_error_msg = "Insufficient rights to install the multi-user \
|
||||||
dependencies, aunch blender with administrator rights."
|
dependencies, aunch blender with administrator rights."
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
# Setup logging policy
|
# Setup logging policy
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@ -58,6 +59,7 @@ def register():
|
|||||||
|
|
||||||
from . import presence
|
from . import presence
|
||||||
from . import operators
|
from . import operators
|
||||||
|
from . import handlers
|
||||||
from . import ui
|
from . import ui
|
||||||
from . import preferences
|
from . import preferences
|
||||||
from . import addon_updater_ops
|
from . import addon_updater_ops
|
||||||
@ -66,6 +68,7 @@ def register():
|
|||||||
addon_updater_ops.register(bl_info)
|
addon_updater_ops.register(bl_info)
|
||||||
presence.register()
|
presence.register()
|
||||||
operators.register()
|
operators.register()
|
||||||
|
handlers.register()
|
||||||
ui.register()
|
ui.register()
|
||||||
except ModuleNotFoundError as e:
|
except ModuleNotFoundError as e:
|
||||||
raise Exception(module_error_msg)
|
raise Exception(module_error_msg)
|
||||||
@ -86,6 +89,7 @@ def register():
|
|||||||
def unregister():
|
def unregister():
|
||||||
from . import presence
|
from . import presence
|
||||||
from . import operators
|
from . import operators
|
||||||
|
from . import handlers
|
||||||
from . import ui
|
from . import ui
|
||||||
from . import preferences
|
from . import preferences
|
||||||
from . import addon_updater_ops
|
from . import addon_updater_ops
|
||||||
@ -95,6 +99,7 @@ def unregister():
|
|||||||
presence.unregister()
|
presence.unregister()
|
||||||
addon_updater_ops.unregister()
|
addon_updater_ops.unregister()
|
||||||
ui.unregister()
|
ui.unregister()
|
||||||
|
handlers.unregister()
|
||||||
operators.unregister()
|
operators.unregister()
|
||||||
preferences.unregister()
|
preferences.unregister()
|
||||||
|
|
||||||
|
@ -1688,10 +1688,7 @@ class GitlabEngine(object):
|
|||||||
# Could clash with tag names and if it does, it will
|
# Could clash with tag names and if it does, it will
|
||||||
# download TAG zip instead of branch zip to get
|
# download TAG zip instead of branch zip to get
|
||||||
# direct path, would need.
|
# direct path, would need.
|
||||||
return "{}{}{}".format(
|
return f"https://gitlab.com/slumber/multi-user/-/jobs/artifacts/{branch}/download?job=build"
|
||||||
self.form_repo_url(updater),
|
|
||||||
"/repository/archive.zip?sha=",
|
|
||||||
branch)
|
|
||||||
|
|
||||||
def get_zip_url(self, sha, updater):
|
def get_zip_url(self, sha, updater):
|
||||||
return "{base}/repository/archive.zip?sha={sha}".format(
|
return "{base}/repository/archive.zip?sha={sha}".format(
|
||||||
|
@ -219,7 +219,7 @@ def load_fcurve(fcurve_data, fcurve):
|
|||||||
def dump_animation_data(datablock):
|
def dump_animation_data(datablock):
|
||||||
animation_data = {}
|
animation_data = {}
|
||||||
if has_action(datablock):
|
if has_action(datablock):
|
||||||
animation_data['action'] = datablock.animation_data.action.name
|
animation_data['action'] = datablock.animation_data.action.uuid
|
||||||
if has_driver(datablock):
|
if has_driver(datablock):
|
||||||
animation_data['drivers'] = []
|
animation_data['drivers'] = []
|
||||||
for driver in datablock.animation_data.drivers:
|
for driver in datablock.animation_data.drivers:
|
||||||
@ -241,8 +241,10 @@ def load_animation_data(animation_data, datablock):
|
|||||||
for driver in animation_data['drivers']:
|
for driver in animation_data['drivers']:
|
||||||
load_driver(datablock, driver)
|
load_driver(datablock, driver)
|
||||||
|
|
||||||
if 'action' in animation_data:
|
action = animation_data.get('action')
|
||||||
datablock.animation_data.action = bpy.data.actions[animation_data['action']]
|
if action:
|
||||||
|
action = resolve_datablock_from_uuid(action, bpy.data.actions)
|
||||||
|
datablock.animation_data.action = action
|
||||||
elif datablock.animation_data.action:
|
elif datablock.animation_data.action:
|
||||||
datablock.animation_data.action = None
|
datablock.animation_data.action = None
|
||||||
|
|
||||||
@ -259,6 +261,8 @@ def resolve_animation_dependencies(datablock):
|
|||||||
|
|
||||||
|
|
||||||
class BlAction(ReplicatedDatablock):
|
class BlAction(ReplicatedDatablock):
|
||||||
|
use_delta = True
|
||||||
|
|
||||||
bl_id = "actions"
|
bl_id = "actions"
|
||||||
bl_class = bpy.types.Action
|
bl_class = bpy.types.Action
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -37,6 +37,8 @@ def get_roll(bone: bpy.types.Bone) -> float:
|
|||||||
|
|
||||||
|
|
||||||
class BlArmature(ReplicatedDatablock):
|
class BlArmature(ReplicatedDatablock):
|
||||||
|
use_delta = True
|
||||||
|
|
||||||
bl_id = "armatures"
|
bl_id = "armatures"
|
||||||
bl_class = bpy.types.Armature
|
bl_class = bpy.types.Armature
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -26,6 +26,8 @@ from .bl_action import dump_animation_data, load_animation_data, resolve_animati
|
|||||||
|
|
||||||
|
|
||||||
class BlCamera(ReplicatedDatablock):
|
class BlCamera(ReplicatedDatablock):
|
||||||
|
use_delta = True
|
||||||
|
|
||||||
bl_id = "cameras"
|
bl_id = "cameras"
|
||||||
bl_class = bpy.types.Camera
|
bl_class = bpy.types.Camera
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -137,6 +137,8 @@ SPLINE_METADATA = [
|
|||||||
|
|
||||||
|
|
||||||
class BlCurve(ReplicatedDatablock):
|
class BlCurve(ReplicatedDatablock):
|
||||||
|
use_delta = True
|
||||||
|
|
||||||
bl_id = "curves"
|
bl_id = "curves"
|
||||||
bl_class = bpy.types.Curve
|
bl_class = bpy.types.Curve
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -29,6 +29,8 @@ POINT = ['co', 'weight_softbody', 'co_deform']
|
|||||||
|
|
||||||
|
|
||||||
class BlLattice(ReplicatedDatablock):
|
class BlLattice(ReplicatedDatablock):
|
||||||
|
use_delta = True
|
||||||
|
|
||||||
bl_id = "lattices"
|
bl_id = "lattices"
|
||||||
bl_class = bpy.types.Lattice
|
bl_class = bpy.types.Lattice
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -26,6 +26,8 @@ from .bl_action import dump_animation_data, load_animation_data, resolve_animati
|
|||||||
|
|
||||||
|
|
||||||
class BlLight(ReplicatedDatablock):
|
class BlLight(ReplicatedDatablock):
|
||||||
|
use_delta = True
|
||||||
|
|
||||||
bl_id = "lights"
|
bl_id = "lights"
|
||||||
bl_class = bpy.types.Light
|
bl_class = bpy.types.Light
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -25,6 +25,8 @@ from replication.protocol import ReplicatedDatablock
|
|||||||
from .bl_datablock import resolve_datablock_from_uuid
|
from .bl_datablock import resolve_datablock_from_uuid
|
||||||
|
|
||||||
class BlLightprobe(ReplicatedDatablock):
|
class BlLightprobe(ReplicatedDatablock):
|
||||||
|
use_delta = True
|
||||||
|
|
||||||
bl_id = "lightprobes"
|
bl_id = "lightprobes"
|
||||||
bl_class = bpy.types.LightProbe
|
bl_class = bpy.types.LightProbe
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -397,11 +397,14 @@ def load_materials_slots(src_materials: list, dst_materials: bpy.types.bpy_prop_
|
|||||||
|
|
||||||
|
|
||||||
class BlMaterial(ReplicatedDatablock):
|
class BlMaterial(ReplicatedDatablock):
|
||||||
|
use_delta = True
|
||||||
|
|
||||||
bl_id = "materials"
|
bl_id = "materials"
|
||||||
bl_class = bpy.types.Material
|
bl_class = bpy.types.Material
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
bl_icon = 'MATERIAL_DATA'
|
bl_icon = 'MATERIAL_DATA'
|
||||||
bl_reload_parent = False
|
bl_reload_parent = False
|
||||||
|
bl_reload_child = True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def construct(data: dict) -> object:
|
def construct(data: dict) -> object:
|
||||||
@ -409,8 +412,6 @@ class BlMaterial(ReplicatedDatablock):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load(data: dict, datablock: object):
|
def load(data: dict, datablock: object):
|
||||||
load_animation_data(data.get('animation_data'), datablock)
|
|
||||||
|
|
||||||
loader = Loader()
|
loader = Loader()
|
||||||
|
|
||||||
is_grease_pencil = data.get('is_grease_pencil')
|
is_grease_pencil = data.get('is_grease_pencil')
|
||||||
@ -427,6 +428,8 @@ class BlMaterial(ReplicatedDatablock):
|
|||||||
datablock.use_nodes = True
|
datablock.use_nodes = True
|
||||||
|
|
||||||
load_node_tree(data['node_tree'], datablock.node_tree)
|
load_node_tree(data['node_tree'], datablock.node_tree)
|
||||||
|
load_animation_data(data.get('nodes_animation_data'), datablock.node_tree)
|
||||||
|
load_animation_data(data.get('animation_data'), datablock)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def dump(datablock: object) -> dict:
|
def dump(datablock: object) -> dict:
|
||||||
@ -494,8 +497,10 @@ class BlMaterial(ReplicatedDatablock):
|
|||||||
data['grease_pencil'] = gp_mat_dumper.dump(datablock.grease_pencil)
|
data['grease_pencil'] = gp_mat_dumper.dump(datablock.grease_pencil)
|
||||||
elif datablock.use_nodes:
|
elif datablock.use_nodes:
|
||||||
data['node_tree'] = dump_node_tree(datablock.node_tree)
|
data['node_tree'] = dump_node_tree(datablock.node_tree)
|
||||||
|
data['nodes_animation_data'] = dump_animation_data(datablock.node_tree)
|
||||||
|
|
||||||
data['animation_data'] = dump_animation_data(datablock)
|
data['animation_data'] = dump_animation_data(datablock)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -509,7 +514,7 @@ class BlMaterial(ReplicatedDatablock):
|
|||||||
|
|
||||||
if datablock.use_nodes:
|
if datablock.use_nodes:
|
||||||
deps.extend(get_node_tree_dependencies(datablock.node_tree))
|
deps.extend(get_node_tree_dependencies(datablock.node_tree))
|
||||||
|
deps.extend(resolve_animation_dependencies(datablock.node_tree))
|
||||||
deps.extend(resolve_animation_dependencies(datablock))
|
deps.extend(resolve_animation_dependencies(datablock))
|
||||||
|
|
||||||
return deps
|
return deps
|
||||||
|
@ -55,6 +55,8 @@ POLYGON = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
class BlMesh(ReplicatedDatablock):
|
class BlMesh(ReplicatedDatablock):
|
||||||
|
use_delta = True
|
||||||
|
|
||||||
bl_id = "meshes"
|
bl_id = "meshes"
|
||||||
bl_class = bpy.types.Mesh
|
bl_class = bpy.types.Mesh
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -65,6 +65,8 @@ def load_metaball_elements(elements_data, elements):
|
|||||||
|
|
||||||
|
|
||||||
class BlMetaball(ReplicatedDatablock):
|
class BlMetaball(ReplicatedDatablock):
|
||||||
|
use_delta = True
|
||||||
|
|
||||||
bl_id = "metaballs"
|
bl_id = "metaballs"
|
||||||
bl_class = bpy.types.MetaBall
|
bl_class = bpy.types.MetaBall
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -28,6 +28,8 @@ 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
|
||||||
|
|
||||||
class BlNodeGroup(ReplicatedDatablock):
|
class BlNodeGroup(ReplicatedDatablock):
|
||||||
|
use_delta = True
|
||||||
|
|
||||||
bl_id = "node_groups"
|
bl_id = "node_groups"
|
||||||
bl_class = bpy.types.NodeTree
|
bl_class = bpy.types.NodeTree
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -493,6 +493,8 @@ def load_modifiers_custom_data(dumped_modifiers: dict, modifiers: bpy.types.bpy_
|
|||||||
|
|
||||||
|
|
||||||
class BlObject(ReplicatedDatablock):
|
class BlObject(ReplicatedDatablock):
|
||||||
|
use_delta = True
|
||||||
|
|
||||||
bl_id = "objects"
|
bl_id = "objects"
|
||||||
bl_class = bpy.types.Object
|
bl_class = bpy.types.Object
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
@ -664,7 +666,11 @@ class BlObject(ReplicatedDatablock):
|
|||||||
'show_all_edges',
|
'show_all_edges',
|
||||||
'show_texture_space',
|
'show_texture_space',
|
||||||
'show_in_front',
|
'show_in_front',
|
||||||
'type'
|
'type',
|
||||||
|
'parent_type',
|
||||||
|
'parent_bone',
|
||||||
|
'track_axis',
|
||||||
|
'up_axis',
|
||||||
]
|
]
|
||||||
|
|
||||||
data = dumper.dump(datablock)
|
data = dumper.dump(datablock)
|
||||||
|
@ -41,6 +41,8 @@ IGNORED_ATTR = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
class BlParticle(ReplicatedDatablock):
|
class BlParticle(ReplicatedDatablock):
|
||||||
|
use_delta = True
|
||||||
|
|
||||||
bl_id = "particles"
|
bl_id = "particles"
|
||||||
bl_class = bpy.types.ParticleSettings
|
bl_class = bpy.types.ParticleSettings
|
||||||
bl_icon = "PARTICLES"
|
bl_icon = "PARTICLES"
|
||||||
|
@ -25,6 +25,8 @@ 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
|
||||||
|
|
||||||
class BlSpeaker(ReplicatedDatablock):
|
class BlSpeaker(ReplicatedDatablock):
|
||||||
|
use_delta = True
|
||||||
|
|
||||||
bl_id = "speakers"
|
bl_id = "speakers"
|
||||||
bl_class = bpy.types.Speaker
|
bl_class = bpy.types.Speaker
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -26,6 +26,8 @@ from .bl_action import dump_animation_data, load_animation_data, resolve_animati
|
|||||||
import bpy.types as T
|
import bpy.types as T
|
||||||
|
|
||||||
class BlTexture(ReplicatedDatablock):
|
class BlTexture(ReplicatedDatablock):
|
||||||
|
use_delta = True
|
||||||
|
|
||||||
bl_id = "textures"
|
bl_id = "textures"
|
||||||
bl_class = bpy.types.Texture
|
bl_class = bpy.types.Texture
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -27,6 +27,8 @@ from .bl_material import dump_materials_slots, load_materials_slots
|
|||||||
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
|
||||||
|
|
||||||
class BlVolume(ReplicatedDatablock):
|
class BlVolume(ReplicatedDatablock):
|
||||||
|
use_delta = True
|
||||||
|
|
||||||
bl_id = "volumes"
|
bl_id = "volumes"
|
||||||
bl_class = bpy.types.Volume
|
bl_class = bpy.types.Volume
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -30,6 +30,8 @@ from .bl_action import dump_animation_data, load_animation_data, resolve_animati
|
|||||||
|
|
||||||
|
|
||||||
class BlWorld(ReplicatedDatablock):
|
class BlWorld(ReplicatedDatablock):
|
||||||
|
use_delta = True
|
||||||
|
|
||||||
bl_id = "worlds"
|
bl_id = "worlds"
|
||||||
bl_class = bpy.types.World
|
bl_class = bpy.types.World
|
||||||
bl_check_common = True
|
bl_check_common = True
|
||||||
|
150
multi_user/handlers.py
Normal file
150
multi_user/handlers.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# ##### END GPL LICENSE BLOCK #####
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from bpy.app.handlers import persistent
|
||||||
|
from replication import porcelain
|
||||||
|
from replication.constants import RP_COMMON, STATE_ACTIVE, STATE_SYNCING, UP
|
||||||
|
from replication.exception import ContextError, NonAuthorizedOperationError
|
||||||
|
from replication.interface import session
|
||||||
|
|
||||||
|
from . import shared_data, utils
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_deps_graph(remove_nodes: bool = False):
|
||||||
|
""" Cleanup the replication graph
|
||||||
|
"""
|
||||||
|
if session and session.state == STATE_ACTIVE:
|
||||||
|
start = utils.current_milli_time()
|
||||||
|
rm_cpt = 0
|
||||||
|
for node in session.repository.graph.values():
|
||||||
|
node.instance = session.repository.rdp.resolve(node.data)
|
||||||
|
if node is None \
|
||||||
|
or (node.state == UP and not node.instance):
|
||||||
|
if remove_nodes:
|
||||||
|
try:
|
||||||
|
porcelain.rm(session.repository,
|
||||||
|
node.uuid,
|
||||||
|
remove_dependencies=False)
|
||||||
|
logging.info(f"Removing {node.uuid}")
|
||||||
|
rm_cpt += 1
|
||||||
|
except NonAuthorizedOperationError:
|
||||||
|
continue
|
||||||
|
logging.info(f"Sanitize took { utils.current_milli_time()-start} ms, removed {rm_cpt} nodes")
|
||||||
|
|
||||||
|
|
||||||
|
def update_external_dependencies():
|
||||||
|
"""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']]
|
||||||
|
for node_id in nodes_ids:
|
||||||
|
node = session.repository.graph.get(node_id)
|
||||||
|
if node and node.owner in [session.repository.username, RP_COMMON]:
|
||||||
|
porcelain.commit(session.repository, node_id)
|
||||||
|
porcelain.push(session.repository, 'origin', node_id)
|
||||||
|
|
||||||
|
|
||||||
|
@persistent
|
||||||
|
def on_scene_update(scene):
|
||||||
|
"""Forward blender depsgraph update to replication
|
||||||
|
"""
|
||||||
|
if session and session.state == STATE_ACTIVE:
|
||||||
|
context = bpy.context
|
||||||
|
blender_depsgraph = bpy.context.view_layer.depsgraph
|
||||||
|
dependency_updates = [u for u in blender_depsgraph.updates]
|
||||||
|
settings = utils.get_preferences()
|
||||||
|
incoming_updates = shared_data.session.applied_updates
|
||||||
|
|
||||||
|
distant_update = [getattr(u.id, 'uuid', None) for u in dependency_updates if getattr(u.id, 'uuid', None) in incoming_updates]
|
||||||
|
if distant_update:
|
||||||
|
for u in distant_update:
|
||||||
|
shared_data.session.applied_updates.remove(u)
|
||||||
|
logging.debug(f"Ignoring distant update of {dependency_updates[0].id.name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
update_external_dependencies()
|
||||||
|
|
||||||
|
# NOTE: maybe we don't need to check each update but only the first
|
||||||
|
for update in reversed(dependency_updates):
|
||||||
|
update_uuid = getattr(update.id, 'uuid', None)
|
||||||
|
if update_uuid:
|
||||||
|
node = session.repository.graph.get(update.id.uuid)
|
||||||
|
check_common = session.repository.rdp.get_implementation(update.id).bl_check_common
|
||||||
|
|
||||||
|
if node and (node.owner == session.repository.username or check_common):
|
||||||
|
logging.debug(f"Evaluate {update.id.name}")
|
||||||
|
if node.state == UP:
|
||||||
|
try:
|
||||||
|
porcelain.commit(session.repository, node.uuid)
|
||||||
|
porcelain.push(session.repository,
|
||||||
|
'origin', node.uuid)
|
||||||
|
except ReferenceError:
|
||||||
|
logging.debug(f"Reference error {node.uuid}")
|
||||||
|
except ContextError as e:
|
||||||
|
logging.debug(e)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(e)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
elif isinstance(update.id, bpy.types.Scene):
|
||||||
|
scn_uuid = porcelain.add(session.repository, update.id)
|
||||||
|
porcelain.commit(session.repository, scn_uuid)
|
||||||
|
porcelain.push(session.repository, 'origin', scn_uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@persistent
|
||||||
|
def resolve_deps_graph(dummy):
|
||||||
|
"""Resolve deps graph
|
||||||
|
|
||||||
|
Temporary solution to resolve each node pointers after a Undo.
|
||||||
|
A future solution should be to avoid storing dataclock reference...
|
||||||
|
|
||||||
|
"""
|
||||||
|
if session and session.state == STATE_ACTIVE:
|
||||||
|
sanitize_deps_graph(remove_nodes=True)
|
||||||
|
|
||||||
|
|
||||||
|
@persistent
|
||||||
|
def load_pre_handler(dummy):
|
||||||
|
if session and session.state in [STATE_ACTIVE, STATE_SYNCING]:
|
||||||
|
bpy.ops.session.stop()
|
||||||
|
|
||||||
|
|
||||||
|
@persistent
|
||||||
|
def update_client_frame(scene):
|
||||||
|
if session and session.state == STATE_ACTIVE:
|
||||||
|
porcelain.update_user_metadata(session.repository, {
|
||||||
|
'frame_current': scene.frame_current
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
bpy.app.handlers.undo_post.append(resolve_deps_graph)
|
||||||
|
bpy.app.handlers.redo_post.append(resolve_deps_graph)
|
||||||
|
|
||||||
|
bpy.app.handlers.load_pre.append(load_pre_handler)
|
||||||
|
bpy.app.handlers.frame_change_pre.append(update_client_frame)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
bpy.app.handlers.undo_post.remove(resolve_deps_graph)
|
||||||
|
bpy.app.handlers.redo_post.remove(resolve_deps_graph)
|
||||||
|
|
||||||
|
bpy.app.handlers.load_pre.remove(load_pre_handler)
|
||||||
|
bpy.app.handlers.frame_change_pre.remove(update_client_frame)
|
Submodule multi_user/libs/replication updated: f0745647e0...ce3a7b4363
@ -27,12 +27,14 @@ import shutil
|
|||||||
import string
|
import string
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
import traceback
|
|
||||||
|
from bpy.props import FloatProperty
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import _pickle as pickle
|
import _pickle as pickle
|
||||||
@ -43,16 +45,17 @@ import bpy
|
|||||||
import mathutils
|
import mathutils
|
||||||
from bpy.app.handlers import persistent
|
from bpy.app.handlers import persistent
|
||||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||||
|
from replication import porcelain
|
||||||
from replication.constants import (COMMITED, FETCHED, RP_COMMON, STATE_ACTIVE,
|
from replication.constants import (COMMITED, FETCHED, RP_COMMON, STATE_ACTIVE,
|
||||||
STATE_INITIAL, STATE_SYNCING, UP)
|
STATE_INITIAL, STATE_SYNCING, UP)
|
||||||
from replication.protocol import DataTranslationProtocol
|
|
||||||
from replication.exception import ContextError, NonAuthorizedOperationError
|
from replication.exception import ContextError, NonAuthorizedOperationError
|
||||||
from replication.interface import session
|
from replication.interface import session
|
||||||
from replication import porcelain
|
|
||||||
from replication.repository import Repository
|
|
||||||
from replication.objects import Node
|
from replication.objects import Node
|
||||||
|
from replication.protocol import DataTranslationProtocol
|
||||||
|
from replication.repository import Repository
|
||||||
|
|
||||||
from . import bl_types, environment, timers, ui, utils
|
from . import bl_types, environment, shared_data, timers, ui, utils
|
||||||
|
from .handlers import on_scene_update, sanitize_deps_graph
|
||||||
from .presence import SessionStatusWidget, renderer, view3d_find
|
from .presence import SessionStatusWidget, renderer, view3d_find
|
||||||
from .timers import registry
|
from .timers import registry
|
||||||
|
|
||||||
@ -97,7 +100,7 @@ def initialize_session():
|
|||||||
|
|
||||||
# Step 2: Load nodes
|
# Step 2: Load nodes
|
||||||
logging.info("Applying nodes")
|
logging.info("Applying nodes")
|
||||||
for node in session.repository.index_sorted:
|
for node in session.repository.heads:
|
||||||
porcelain.apply(session.repository, node)
|
porcelain.apply(session.repository, node)
|
||||||
|
|
||||||
logging.info("Registering timers")
|
logging.info("Registering timers")
|
||||||
@ -110,7 +113,7 @@ def initialize_session():
|
|||||||
utils.flush_history()
|
utils.flush_history()
|
||||||
|
|
||||||
# Step 6: Launch deps graph update handling
|
# Step 6: Launch deps graph update handling
|
||||||
bpy.app.handlers.depsgraph_update_post.append(depsgraph_evaluation)
|
bpy.app.handlers.depsgraph_update_post.append(on_scene_update)
|
||||||
|
|
||||||
|
|
||||||
@session_callback('on_exit')
|
@session_callback('on_exit')
|
||||||
@ -130,8 +133,8 @@ def on_connection_end(reason="none"):
|
|||||||
|
|
||||||
stop_modal_executor = True
|
stop_modal_executor = True
|
||||||
|
|
||||||
if depsgraph_evaluation in bpy.app.handlers.depsgraph_update_post:
|
if on_scene_update in bpy.app.handlers.depsgraph_update_post:
|
||||||
bpy.app.handlers.depsgraph_update_post.remove(depsgraph_evaluation)
|
bpy.app.handlers.depsgraph_update_post.remove(on_scene_update)
|
||||||
|
|
||||||
# Step 3: remove file handled
|
# Step 3: remove file handled
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
@ -160,7 +163,7 @@ 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 = runtime_settings.password
|
admin_pass = settings.password
|
||||||
|
|
||||||
users.clear()
|
users.clear()
|
||||||
deleyables.clear()
|
deleyables.clear()
|
||||||
@ -601,9 +604,9 @@ class SessionApply(bpy.types.Operator):
|
|||||||
node_ref = session.repository.graph.get(self.target)
|
node_ref = session.repository.graph.get(self.target)
|
||||||
porcelain.apply(session.repository,
|
porcelain.apply(session.repository,
|
||||||
self.target,
|
self.target,
|
||||||
force=True,
|
force=True)
|
||||||
force_dependencies=self.reset_dependencies)
|
|
||||||
impl = session.repository.rdp.get_implementation(node_ref.instance)
|
impl = session.repository.rdp.get_implementation(node_ref.instance)
|
||||||
|
# NOTE: find another way to handle child and parent automatic reloading
|
||||||
if impl.bl_reload_parent:
|
if impl.bl_reload_parent:
|
||||||
for parent in session.repository.graph.get_parents(self.target):
|
for parent in session.repository.graph.get_parents(self.target):
|
||||||
logging.debug(f"Refresh parent {parent}")
|
logging.debug(f"Refresh parent {parent}")
|
||||||
@ -611,6 +614,11 @@ class SessionApply(bpy.types.Operator):
|
|||||||
porcelain.apply(session.repository,
|
porcelain.apply(session.repository,
|
||||||
parent.uuid,
|
parent.uuid,
|
||||||
force=True)
|
force=True)
|
||||||
|
if hasattr(impl, 'bl_reload_child') and impl.bl_reload_child:
|
||||||
|
for dep in node_ref.dependencies:
|
||||||
|
porcelain.apply(session.repository,
|
||||||
|
dep,
|
||||||
|
force=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.report({'ERROR'}, repr(e))
|
self.report({'ERROR'}, repr(e))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@ -634,7 +642,7 @@ class SessionCommit(bpy.types.Operator):
|
|||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
try:
|
try:
|
||||||
porcelain.commit(session.repository, self.target)
|
porcelain.commit(session.repository, self.target)
|
||||||
porcelain.push(session.repository, 'origin', self.target)
|
porcelain.push(session.repository, 'origin', self.target, force=True)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.report({'ERROR'}, repr(e))
|
self.report({'ERROR'}, repr(e))
|
||||||
@ -682,6 +690,7 @@ class SessionPurgeOperator(bpy.types.Operator):
|
|||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
try:
|
try:
|
||||||
sanitize_deps_graph(remove_nodes=True)
|
sanitize_deps_graph(remove_nodes=True)
|
||||||
|
porcelain.purge_orphan_nodes(session.repository)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.report({'ERROR'}, repr(e))
|
self.report({'ERROR'}, repr(e))
|
||||||
|
|
||||||
@ -714,7 +723,6 @@ class SessionNotifyOperator(bpy.types.Operator):
|
|||||||
layout = self.layout
|
layout = self.layout
|
||||||
layout.row().label(text=self.message)
|
layout.row().label(text=self.message)
|
||||||
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
return context.window_manager.invoke_props_dialog(self)
|
return context.window_manager.invoke_props_dialog(self)
|
||||||
|
|
||||||
@ -821,6 +829,75 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
|
|||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
class SessionPresetServerAdd(bpy.types.Operator):
|
||||||
|
"""Add a server to the server list preset"""
|
||||||
|
bl_idname = "session.preset_server_add"
|
||||||
|
bl_label = "add server preset"
|
||||||
|
bl_description = "add the current server to the server preset list"
|
||||||
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
|
name : bpy.props.StringProperty(default="server_preset")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
assert(context)
|
||||||
|
return context.window_manager.invoke_props_dialog(self)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
|
||||||
|
col = layout.column()
|
||||||
|
settings = utils.get_preferences()
|
||||||
|
|
||||||
|
col.prop(settings, "server_name", text="server name")
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
assert(context)
|
||||||
|
|
||||||
|
settings = utils.get_preferences()
|
||||||
|
|
||||||
|
existing_preset = settings.server_preset.get(settings.server_name)
|
||||||
|
|
||||||
|
new_server = existing_preset if existing_preset else settings.server_preset.add()
|
||||||
|
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'}
|
||||||
|
|
||||||
|
|
||||||
|
class SessionPresetServerRemove(bpy.types.Operator):
|
||||||
|
"""Remove a server to the server list preset"""
|
||||||
|
bl_idname = "session.preset_server_remove"
|
||||||
|
bl_label = "remove server preset"
|
||||||
|
bl_description = "remove the current server from the server preset list"
|
||||||
|
bl_options = {"REGISTER"}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
assert(context)
|
||||||
|
|
||||||
|
settings = utils.get_preferences()
|
||||||
|
|
||||||
|
settings.server_preset.remove(settings.server_preset.find(settings.server_preset_interface))
|
||||||
|
|
||||||
|
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)')
|
||||||
@ -843,113 +920,11 @@ classes = (
|
|||||||
SessionLoadSaveOperator,
|
SessionLoadSaveOperator,
|
||||||
SessionStopAutoSaveOperator,
|
SessionStopAutoSaveOperator,
|
||||||
SessionPurgeOperator,
|
SessionPurgeOperator,
|
||||||
|
SessionPresetServerAdd,
|
||||||
|
SessionPresetServerRemove,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def update_external_dependencies():
|
|
||||||
nodes_ids = [n.uuid for n in session.repository.graph.values() if n.data['type_id'] in ['WindowsPath', 'PosixPath']]
|
|
||||||
for node_id in nodes_ids:
|
|
||||||
node = session.repository.graph.get(node_id)
|
|
||||||
if node and node.owner in [session.repository.username, RP_COMMON]:
|
|
||||||
porcelain.commit(session.repository, node_id)
|
|
||||||
porcelain.push(session.repository,'origin', node_id)
|
|
||||||
|
|
||||||
|
|
||||||
def sanitize_deps_graph(remove_nodes: bool = False):
|
|
||||||
""" Cleanup the replication graph
|
|
||||||
"""
|
|
||||||
if session and session.state == STATE_ACTIVE:
|
|
||||||
start = utils.current_milli_time()
|
|
||||||
rm_cpt = 0
|
|
||||||
for node in session.repository.graph.values():
|
|
||||||
node.instance = session.repository.rdp.resolve(node.data)
|
|
||||||
if node is None \
|
|
||||||
or (node.state == UP and not node.instance):
|
|
||||||
if remove_nodes:
|
|
||||||
try:
|
|
||||||
porcelain.rm(session.repository,
|
|
||||||
node.uuid,
|
|
||||||
remove_dependencies=False)
|
|
||||||
logging.info(f"Removing {node.uuid}")
|
|
||||||
rm_cpt += 1
|
|
||||||
except NonAuthorizedOperationError:
|
|
||||||
continue
|
|
||||||
logging.info(f"Sanitize took { utils.current_milli_time()-start} ms, removed {rm_cpt} nodes")
|
|
||||||
|
|
||||||
|
|
||||||
@persistent
|
|
||||||
def resolve_deps_graph(dummy):
|
|
||||||
"""Resolve deps graph
|
|
||||||
|
|
||||||
Temporary solution to resolve each node pointers after a Undo.
|
|
||||||
A future solution should be to avoid storing dataclock reference...
|
|
||||||
|
|
||||||
"""
|
|
||||||
if session and session.state == STATE_ACTIVE:
|
|
||||||
sanitize_deps_graph(remove_nodes=True)
|
|
||||||
|
|
||||||
|
|
||||||
@persistent
|
|
||||||
def load_pre_handler(dummy):
|
|
||||||
if session and session.state in [STATE_ACTIVE, STATE_SYNCING]:
|
|
||||||
bpy.ops.session.stop()
|
|
||||||
|
|
||||||
|
|
||||||
@persistent
|
|
||||||
def update_client_frame(scene):
|
|
||||||
if session and session.state == STATE_ACTIVE:
|
|
||||||
porcelain.update_user_metadata(session.repository, {
|
|
||||||
'frame_current': scene.frame_current
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@persistent
|
|
||||||
def depsgraph_evaluation(scene):
|
|
||||||
if session and session.state == STATE_ACTIVE:
|
|
||||||
context = bpy.context
|
|
||||||
blender_depsgraph = bpy.context.view_layer.depsgraph
|
|
||||||
dependency_updates = [u for u in blender_depsgraph.updates]
|
|
||||||
settings = utils.get_preferences()
|
|
||||||
|
|
||||||
update_external_dependencies()
|
|
||||||
|
|
||||||
is_internal = [u for u in dependency_updates if u.is_updated_geometry or u.is_updated_shading or u.is_updated_transform]
|
|
||||||
|
|
||||||
# NOTE: maybe we don't need to check each update but only the first
|
|
||||||
if not is_internal:
|
|
||||||
return
|
|
||||||
for update in reversed(dependency_updates):
|
|
||||||
# Is the object tracked ?
|
|
||||||
if update.id.uuid:
|
|
||||||
# Retrieve local version
|
|
||||||
node = session.repository.graph.get(update.id.uuid)
|
|
||||||
check_common = session.repository.rdp.get_implementation(update.id).bl_check_common
|
|
||||||
# Check our right on this update:
|
|
||||||
# - if its ours or ( under common and diff), launch the
|
|
||||||
# update process
|
|
||||||
# - if its to someone else, ignore the update
|
|
||||||
if node and (node.owner == session.repository.username or check_common):
|
|
||||||
if node.state == UP:
|
|
||||||
try:
|
|
||||||
porcelain.commit(session.repository, node.uuid)
|
|
||||||
porcelain.push(session.repository, 'origin', node.uuid)
|
|
||||||
except ReferenceError:
|
|
||||||
logging.debug(f"Reference error {node.uuid}")
|
|
||||||
except ContextError as e:
|
|
||||||
logging.debug(e)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(e)
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
# A new scene is created
|
|
||||||
elif isinstance(update.id, bpy.types.Scene):
|
|
||||||
ref = session.repository.get_node_by_datablock(update.id)
|
|
||||||
if ref:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
scn_uuid = porcelain.add(session.repository, update.id)
|
|
||||||
porcelain.commit(session.node_id, scn_uuid)
|
|
||||||
porcelain.push(session.repository,'origin', scn_uuid)
|
|
||||||
def register():
|
def register():
|
||||||
from bpy.utils import register_class
|
from bpy.utils import register_class
|
||||||
|
|
||||||
@ -957,13 +932,6 @@ def register():
|
|||||||
register_class(cls)
|
register_class(cls)
|
||||||
|
|
||||||
|
|
||||||
bpy.app.handlers.undo_post.append(resolve_deps_graph)
|
|
||||||
bpy.app.handlers.redo_post.append(resolve_deps_graph)
|
|
||||||
|
|
||||||
bpy.app.handlers.load_pre.append(load_pre_handler)
|
|
||||||
bpy.app.handlers.frame_change_pre.append(update_client_frame)
|
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
if session and session.state == STATE_ACTIVE:
|
if session and session.state == STATE_ACTIVE:
|
||||||
session.disconnect()
|
session.disconnect()
|
||||||
@ -971,9 +939,3 @@ def unregister():
|
|||||||
from bpy.utils import unregister_class
|
from bpy.utils import unregister_class
|
||||||
for cls in reversed(classes):
|
for cls in reversed(classes):
|
||||||
unregister_class(cls)
|
unregister_class(cls)
|
||||||
|
|
||||||
bpy.app.handlers.undo_post.remove(resolve_deps_graph)
|
|
||||||
bpy.app.handlers.redo_post.remove(resolve_deps_graph)
|
|
||||||
|
|
||||||
bpy.app.handlers.load_pre.remove(load_pre_handler)
|
|
||||||
bpy.app.handlers.frame_change_pre.remove(update_client_frame)
|
|
||||||
|
@ -33,6 +33,19 @@ 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])$")
|
||||||
|
|
||||||
|
DEFAULT_PRESETS = {
|
||||||
|
"localhost" : {
|
||||||
|
"server_ip": "localhost",
|
||||||
|
"server_port": 5555,
|
||||||
|
"server_password": "admin"
|
||||||
|
},
|
||||||
|
"public session" : {
|
||||||
|
"server_ip": "51.75.71.183",
|
||||||
|
"server_port": 5555,
|
||||||
|
"server_password": ""
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
def randomColor():
|
def randomColor():
|
||||||
"""Generate a random color """
|
"""Generate a random color """
|
||||||
r = random.random()
|
r = random.random()
|
||||||
@ -65,8 +78,11 @@ 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)
|
||||||
@ -93,6 +109,10 @@ class ReplicatedDatablock(bpy.types.PropertyGroup):
|
|||||||
auto_push: bpy.props.BoolProperty(default=True)
|
auto_push: bpy.props.BoolProperty(default=True)
|
||||||
icon: bpy.props.StringProperty()
|
icon: bpy.props.StringProperty()
|
||||||
|
|
||||||
|
class ServerPreset(bpy.types.PropertyGroup):
|
||||||
|
server_ip: bpy.props.StringProperty()
|
||||||
|
server_port: bpy.props.IntProperty(default=5555)
|
||||||
|
server_password: bpy.props.StringProperty(default="admin", subtype = "PASSWORD")
|
||||||
|
|
||||||
def set_sync_render_settings(self, value):
|
def set_sync_render_settings(self, value):
|
||||||
self['sync_render_settings'] = value
|
self['sync_render_settings'] = value
|
||||||
@ -145,7 +165,7 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
|||||||
ip: bpy.props.StringProperty(
|
ip: bpy.props.StringProperty(
|
||||||
name="ip",
|
name="ip",
|
||||||
description='Distant host ip',
|
description='Distant host ip',
|
||||||
default="127.0.0.1",
|
default="localhost",
|
||||||
update=update_ip)
|
update=update_ip)
|
||||||
username: bpy.props.StringProperty(
|
username: bpy.props.StringProperty(
|
||||||
name="Username",
|
name="Username",
|
||||||
@ -160,6 +180,17 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
|||||||
description='Distant host port',
|
description='Distant host port',
|
||||||
default=5555
|
default=5555
|
||||||
)
|
)
|
||||||
|
server_name: bpy.props.StringProperty(
|
||||||
|
name="server_name",
|
||||||
|
description="Custom name of the server",
|
||||||
|
default='localhost',
|
||||||
|
)
|
||||||
|
password: bpy.props.StringProperty(
|
||||||
|
name="password",
|
||||||
|
default=random_string_digits(),
|
||||||
|
description='Session password',
|
||||||
|
subtype='PASSWORD'
|
||||||
|
)
|
||||||
sync_flags: bpy.props.PointerProperty(
|
sync_flags: bpy.props.PointerProperty(
|
||||||
type=ReplicationFlags
|
type=ReplicationFlags
|
||||||
)
|
)
|
||||||
@ -321,6 +352,25 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
|||||||
max=59
|
max=59
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Server preset
|
||||||
|
def server_list_callback(scene, context):
|
||||||
|
settings = get_preferences()
|
||||||
|
enum = []
|
||||||
|
for i in settings.server_preset:
|
||||||
|
enum.append((i.name, i.name, ""))
|
||||||
|
return enum
|
||||||
|
|
||||||
|
server_preset: bpy.props.CollectionProperty(
|
||||||
|
name="server preset",
|
||||||
|
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(
|
||||||
description="Choose a name for the category of the panel",
|
description="Choose a name for the category of the panel",
|
||||||
@ -420,6 +470,18 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
|||||||
new_db.bl_name = impl.bl_id
|
new_db.bl_name = impl.bl_id
|
||||||
|
|
||||||
|
|
||||||
|
# custom at launch server preset
|
||||||
|
def generate_default_presets(self):
|
||||||
|
for preset_name, preset_data in DEFAULT_PRESETS.items():
|
||||||
|
existing_preset = self.server_preset.get(preset_name)
|
||||||
|
if existing_preset :
|
||||||
|
continue
|
||||||
|
new_server = self.server_preset.add()
|
||||||
|
new_server.name = preset_name
|
||||||
|
new_server.server_ip = preset_data.get('server_ip')
|
||||||
|
new_server.server_port = preset_data.get('server_port')
|
||||||
|
new_server.server_password = preset_data.get('server_password',None)
|
||||||
|
|
||||||
|
|
||||||
def client_list_callback(scene, context):
|
def client_list_callback(scene, context):
|
||||||
from . import operators
|
from . import operators
|
||||||
@ -501,12 +563,6 @@ class SessionProps(bpy.types.PropertyGroup):
|
|||||||
description='Connect as admin',
|
description='Connect as admin',
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
password: bpy.props.StringProperty(
|
|
||||||
name="password",
|
|
||||||
default=random_string_digits(),
|
|
||||||
description='Session password',
|
|
||||||
subtype='PASSWORD'
|
|
||||||
)
|
|
||||||
internet_ip: bpy.props.StringProperty(
|
internet_ip: bpy.props.StringProperty(
|
||||||
name="internet ip",
|
name="internet ip",
|
||||||
default="no found",
|
default="no found",
|
||||||
@ -528,6 +584,7 @@ classes = (
|
|||||||
SessionProps,
|
SessionProps,
|
||||||
ReplicationFlags,
|
ReplicationFlags,
|
||||||
ReplicatedDatablock,
|
ReplicatedDatablock,
|
||||||
|
ServerPreset,
|
||||||
SessionPrefs,
|
SessionPrefs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -543,6 +600,10 @@ def register():
|
|||||||
logging.debug('Generating bl_types preferences')
|
logging.debug('Generating bl_types preferences')
|
||||||
prefs.generate_supported_types()
|
prefs.generate_supported_types()
|
||||||
|
|
||||||
|
# at launch server presets
|
||||||
|
prefs.generate_default_presets()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
from bpy.utils import unregister_class
|
from bpy.utils import unregister_class
|
||||||
|
@ -94,15 +94,35 @@ 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) -> 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
|
:return: list of 8 points [(x,y,z),...], list of 12 link between these points [(1,2),...]
|
||||||
:type radius: float
|
|
||||||
:return: list of 8 points [(x,y,z),...]
|
|
||||||
"""
|
"""
|
||||||
|
radius = 1.0 # Radius of the bounding box
|
||||||
|
vertex_indices = (
|
||||||
|
(0, 1), (0, 2), (1, 3), (2, 3),
|
||||||
|
(4, 5), (4, 6), (5, 7), (6, 7),
|
||||||
|
(0, 4), (1, 5), (2, 6), (3, 7))
|
||||||
|
|
||||||
|
if 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, 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(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 +132,37 @@ 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) -> 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 radius: bounding box radius
|
||||||
|
:type radius: float
|
||||||
|
: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)
|
||||||
|
vertex_pos += vertex_pos_temp
|
||||||
|
vertex_indices_list_temp = list(list(indice) for indice in vertex_indices_temp)
|
||||||
|
for indice in vertex_indices_list_temp:
|
||||||
|
indice[0] += 8*obj_index
|
||||||
|
indice[1] += 8*obj_index
|
||||||
|
vertex_indices_temp = tuple(tuple(indice) for indice in vertex_indices_list_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
|
||||||
@ -296,36 +344,14 @@ class UserSelectionWidget(Widget):
|
|||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
user_selection = self.data.get('selected_objects')
|
user_selection = self.data.get('selected_objects')
|
||||||
for select_ob in user_selection:
|
for select_obj in user_selection:
|
||||||
ob = find_from_attr("uuid", select_ob, bpy.data.objects)
|
obj = find_from_attr("uuid", select_obj, bpy.data.objects)
|
||||||
if not ob:
|
if not obj:
|
||||||
return
|
return
|
||||||
|
if obj.instance_collection:
|
||||||
vertex_pos = bbox_from_obj(ob, 1.0)
|
vertex_pos, vertex_indices = bbox_from_instance_collection(obj)
|
||||||
vertex_indices = (
|
else :
|
||||||
(0, 1), (1, 2), (2, 3), (0, 3),
|
vertex_pos, vertex_indices = bbox_from_obj(obj)
|
||||||
(4, 5), (5, 6), (6, 7), (4, 7),
|
|
||||||
(0, 4), (1, 5), (2, 6), (3, 7))
|
|
||||||
|
|
||||||
if ob.instance_collection:
|
|
||||||
for obj in ob.instance_collection.objects:
|
|
||||||
if obj.type == 'MESH' and hasattr(obj, 'bound_box'):
|
|
||||||
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')
|
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
|
||||||
batch = batch_for_shader(
|
batch = batch_for_shader(
|
||||||
@ -338,7 +364,6 @@ class UserSelectionWidget(Widget):
|
|||||||
shader.uniform_float("color", self.data.get('color'))
|
shader.uniform_float("color", self.data.get('color'))
|
||||||
batch.draw(shader)
|
batch.draw(shader)
|
||||||
|
|
||||||
|
|
||||||
class UserNameWidget(Widget):
|
class UserNameWidget(Widget):
|
||||||
draw_type = 'POST_PIXEL'
|
draw_type = 'POST_PIXEL'
|
||||||
|
|
||||||
|
48
multi_user/shared_data.py
Normal file
48
multi_user/shared_data.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# ##### 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 #####
|
||||||
|
|
||||||
|
from replication.constants import STATE_INITIAL
|
||||||
|
|
||||||
|
|
||||||
|
class SessionData():
|
||||||
|
""" A structure to share easily the current session data across the addon
|
||||||
|
modules.
|
||||||
|
This object will completely replace the Singleton lying in replication
|
||||||
|
interface module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.repository = None # The current repository
|
||||||
|
self.remote = None # The active remote
|
||||||
|
self.server = None
|
||||||
|
self.applied_updates = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
if self.remote is None:
|
||||||
|
return STATE_INITIAL
|
||||||
|
else:
|
||||||
|
return self.remote.connection_status
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.remote = None
|
||||||
|
self.repository = None
|
||||||
|
self.server = None
|
||||||
|
self.applied_updates = []
|
||||||
|
|
||||||
|
|
||||||
|
session = SessionData()
|
@ -31,6 +31,8 @@ from .presence import (UserFrustumWidget, UserNameWidget, 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)
|
||||||
|
|
||||||
|
from . import shared_data
|
||||||
|
|
||||||
this = sys.modules[__name__]
|
this = sys.modules[__name__]
|
||||||
|
|
||||||
# Registered timers
|
# Registered timers
|
||||||
@ -114,6 +116,7 @@ class ApplyTimer(Timer):
|
|||||||
|
|
||||||
if node_ref.state == FETCHED:
|
if node_ref.state == FETCHED:
|
||||||
try:
|
try:
|
||||||
|
shared_data.session.applied_updates.append(node)
|
||||||
porcelain.apply(session.repository, node)
|
porcelain.apply(session.repository, node)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Fail to apply {node_ref.uuid}")
|
logging.error(f"Fail to apply {node_ref.uuid}")
|
||||||
@ -126,6 +129,11 @@ class ApplyTimer(Timer):
|
|||||||
porcelain.apply(session.repository,
|
porcelain.apply(session.repository,
|
||||||
parent.uuid,
|
parent.uuid,
|
||||||
force=True)
|
force=True)
|
||||||
|
if hasattr(impl, 'bl_reload_child') and impl.bl_reload_child:
|
||||||
|
for dep in node_ref.dependencies:
|
||||||
|
porcelain.apply(session.repository,
|
||||||
|
dep,
|
||||||
|
force=True)
|
||||||
|
|
||||||
|
|
||||||
class DynamicRightSelectTimer(Timer):
|
class DynamicRightSelectTimer(Timer):
|
||||||
@ -251,6 +259,7 @@ class DynamicRightSelectTimer(Timer):
|
|||||||
is_selectable = not session.repository.is_node_readonly(object_uuid)
|
is_selectable = not session.repository.is_node_readonly(object_uuid)
|
||||||
if obj.hide_select != is_selectable:
|
if obj.hide_select != is_selectable:
|
||||||
obj.hide_select = is_selectable
|
obj.hide_select = is_selectable
|
||||||
|
shared_data.session.applied_updates.append(object_uuid)
|
||||||
|
|
||||||
|
|
||||||
class ClientUpdate(Timer):
|
class ClientUpdate(Timer):
|
||||||
|
@ -157,6 +157,12 @@ class SESSION_PT_settings_network(bpy.types.Panel):
|
|||||||
row.prop(runtime_settings, "session_mode", expand=True)
|
row.prop(runtime_settings, "session_mode", expand=True)
|
||||||
row = layout.row()
|
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()
|
box = row.box()
|
||||||
|
|
||||||
if runtime_settings.session_mode == 'HOST':
|
if runtime_settings.session_mode == 'HOST':
|
||||||
@ -168,7 +174,7 @@ class SESSION_PT_settings_network(bpy.types.Panel):
|
|||||||
row.prop(settings, "init_method", text="")
|
row.prop(settings, "init_method", text="")
|
||||||
row = box.row()
|
row = box.row()
|
||||||
row.label(text="Admin password:")
|
row.label(text="Admin password:")
|
||||||
row.prop(runtime_settings, "password", text="")
|
row.prop(settings, "password", text="")
|
||||||
row = box.row()
|
row = box.row()
|
||||||
row.operator("session.start", text="HOST").host = True
|
row.operator("session.start", text="HOST").host = True
|
||||||
else:
|
else:
|
||||||
@ -184,11 +190,10 @@ class SESSION_PT_settings_network(bpy.types.Panel):
|
|||||||
if runtime_settings.admin:
|
if runtime_settings.admin:
|
||||||
row = box.row()
|
row = box.row()
|
||||||
row.label(text="Password:")
|
row.label(text="Password:")
|
||||||
row.prop(runtime_settings, "password", text="")
|
row.prop(settings, "password", text="")
|
||||||
row = box.row()
|
row = box.row()
|
||||||
row.operator("session.start", text="CONNECT").host = False
|
row.operator("session.start", text="CONNECT").host = False
|
||||||
|
|
||||||
|
|
||||||
class SESSION_PT_settings_user(bpy.types.Panel):
|
class SESSION_PT_settings_user(bpy.types.Panel):
|
||||||
bl_idname = "MULTIUSER_SETTINGS_USER_PT_panel"
|
bl_idname = "MULTIUSER_SETTINGS_USER_PT_panel"
|
||||||
bl_label = "User info"
|
bl_label = "User info"
|
||||||
|
@ -38,6 +38,14 @@ from replication.constants import (STATE_ACTIVE, STATE_AUTH,
|
|||||||
STATE_LOBBY,
|
STATE_LOBBY,
|
||||||
CONNECTING)
|
CONNECTING)
|
||||||
|
|
||||||
|
CLEARED_DATABLOCKS = ['actions', 'armatures', 'cache_files', 'cameras',
|
||||||
|
'collections', 'curves', 'filepath', 'fonts',
|
||||||
|
'grease_pencils', 'images', 'lattices', 'libraries',
|
||||||
|
'lightprobes', 'lights', 'linestyles', 'masks',
|
||||||
|
'materials', 'meshes', 'metaballs', 'movieclips',
|
||||||
|
'node_groups', 'objects', 'paint_curves', 'particles',
|
||||||
|
'scenes', 'shape_keys', 'sounds', 'speakers', 'texts',
|
||||||
|
'textures', 'volumes', 'worlds']
|
||||||
|
|
||||||
def find_from_attr(attr_name, attr_value, list):
|
def find_from_attr(attr_name, attr_value, list):
|
||||||
for item in list:
|
for item in list:
|
||||||
@ -101,17 +109,25 @@ def get_state_str(state):
|
|||||||
|
|
||||||
|
|
||||||
def clean_scene():
|
def clean_scene():
|
||||||
for type_name in dir(bpy.data):
|
for type_name in CLEARED_DATABLOCKS:
|
||||||
try:
|
sub_collection_to_avoid = [
|
||||||
|
bpy.data.linestyles.get('LineStyle'),
|
||||||
|
bpy.data.materials.get('Dots Stroke')
|
||||||
|
]
|
||||||
|
|
||||||
type_collection = getattr(bpy.data, type_name)
|
type_collection = getattr(bpy.data, type_name)
|
||||||
for item in type_collection:
|
items_to_remove = [i for i in type_collection if i not in sub_collection_to_avoid]
|
||||||
|
for item in items_to_remove:
|
||||||
|
try:
|
||||||
type_collection.remove(item)
|
type_collection.remove(item)
|
||||||
|
logging.info(item.name)
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Clear sequencer
|
# Clear sequencer
|
||||||
bpy.context.scene.sequence_editor_clear()
|
bpy.context.scene.sequence_editor_clear()
|
||||||
|
|
||||||
|
|
||||||
def get_selected_objects(scene, active_view_layer):
|
def get_selected_objects(scene, active_view_layer):
|
||||||
return [obj.uuid for obj in scene.objects if obj.select_get(view_layer=active_view_layer)]
|
return [obj.uuid for obj in scene.objects if obj.select_get(view_layer=active_view_layer)]
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ def main():
|
|||||||
if len(sys.argv) > 2:
|
if len(sys.argv) > 2:
|
||||||
blender_rev = sys.argv[2]
|
blender_rev = sys.argv[2]
|
||||||
else:
|
else:
|
||||||
blender_rev = "2.92.0"
|
blender_rev = "2.93.0"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
exit_val = BAT.test_blender_addon(addon_path=addon, blender_revision=blender_rev)
|
exit_val = BAT.test_blender_addon(addon_path=addon, blender_revision=blender_rev)
|
||||||
|
Reference in New Issue
Block a user