Compare commits
101 Commits
224-doc-up
...
259-ensure
Author | SHA1 | Date | |
---|---|---|---|
9452dc010d | |||
4f71e41436 | |||
38e88d0a4c | |||
eff81e976e | |||
60c718ca61 | |||
a87e74842b | |||
6fd8a32959 | |||
0c26b9c6c4 | |||
037be421cb | |||
8a3adc6cfa | |||
5b45b0a50a | |||
a0d5f0ae02 | |||
5b210e6d8b | |||
6e63055b20 | |||
12355b6457 | |||
74ad4e5e1f | |||
2a88c3e5ac | |||
4c42a5be92 | |||
757ee7015a | |||
15d66579c6 | |||
4128a47b88 | |||
689a565c75 | |||
c5f1bf1176 | |||
4dc6781c94 | |||
5311e55208 | |||
4cb64e5e77 | |||
ff67b581b1 | |||
f7bec3fc08 | |||
5e929db3ee | |||
629f2e1cdb | |||
b8fed806ed | |||
8190846b59 | |||
c228b6ad7f | |||
48651ce890 | |||
26847cf459 | |||
bfa6991c00 | |||
0c60c86775 | |||
70b6f9bcfa | |||
8d176b55e4 | |||
4c0356e724 | |||
6b04d1d8d6 | |||
edfcdd8867 | |||
bdd6599614 | |||
6efd1321ce | |||
047bd47048 | |||
d32cbb7b30 | |||
adabce3822 | |||
62f52db5b2 | |||
745f45b682 | |||
4b7573234a | |||
f84860f520 | |||
c7ee67d4dd | |||
7ed4644b75 | |||
e0c4a17be9 | |||
2a6181b832 | |||
0f7c9adec5 | |||
f094ec097c | |||
2495b5b0e7 | |||
cc829b66d1 | |||
97cec4f9af | |||
3669aafcff | |||
dfcfb84c20 | |||
5390e1a60c | |||
2910ea654b | |||
ff2ecec18b | |||
7555b1332a | |||
690e450349 | |||
de32bd89e3 | |||
50e86aea15 | |||
c05a12343c | |||
a09193fba2 | |||
60e21f2b8e | |||
421f00879f | |||
5ac61b5348 | |||
189e5c6cf1 | |||
964e6a8c63 | |||
80c81dc934 | |||
563fdb693d | |||
a64eea3cea | |||
03ad7c0066 | |||
d685573834 | |||
0681b53141 | |||
6f02b38b0e | |||
92c773dae9 | |||
f48ade6390 | |||
63c4501b88 | |||
06e21c86ce | |||
e28d3860da | |||
7b247372fb | |||
9d484b00e9 | |||
de9255f71c | |||
99528ea3e0 | |||
bb342951a5 | |||
438a79177b | |||
08fc49c40f | |||
d7e25b1192 | |||
1671422143 | |||
a9620c0752 | |||
ac84509b83 | |||
69565b3852 | |||
57fdd492ef |
1
.gitignore
vendored
1
.gitignore
vendored
@ -15,3 +15,4 @@ _build
|
||||
# ignore generated zip generated from blender_addon_tester
|
||||
*.zip
|
||||
libs
|
||||
venv
|
@ -1,13 +1,8 @@
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
- deploy
|
||||
- doc
|
||||
|
||||
|
||||
|
||||
include:
|
||||
- local: .gitlab/ci/test.gitlab-ci.yml
|
||||
- local: .gitlab/ci/build.gitlab-ci.yml
|
||||
- local: .gitlab/ci/deploy.gitlab-ci.yml
|
||||
- local: .gitlab/ci/doc.gitlab-ci.yml
|
||||
|
@ -1,6 +1,5 @@
|
||||
build:
|
||||
stage: build
|
||||
needs: ["test"]
|
||||
image: debian:stable-slim
|
||||
script:
|
||||
- rm -rf tests .git .gitignore script
|
||||
@ -8,5 +7,3 @@ build:
|
||||
name: multi_user
|
||||
paths:
|
||||
- multi_user
|
||||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
|
@ -1,21 +0,0 @@
|
||||
deploy:
|
||||
stage: deploy
|
||||
needs: ["build"]
|
||||
image: slumber/docker-python
|
||||
variables:
|
||||
DOCKER_DRIVER: overlay2
|
||||
DOCKER_TLS_CERTDIR: "/certs"
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
|
||||
services:
|
||||
- docker:19.03.12-dind
|
||||
|
||||
script:
|
||||
- RP_VERSION="$(python scripts/get_replication_version.py)"
|
||||
- VERSION="$(python scripts/get_addon_version.py)"
|
||||
- echo "Building docker image with replication ${RP_VERSION}"
|
||||
- docker build --build-arg replication_version=${RP_VERSION} --build-arg version={VERSION} -t registry.gitlab.com/slumber/multi-user/multi-user-server:${VERSION} ./scripts/docker_server
|
||||
- echo "Pushing to gitlab registry ${VERSION}"
|
||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
- docker tag registry.gitlab.com/slumber/multi-user/multi-user-server:${VERSION} registry.gitlab.com/slumber/multi-user/multi-user-server:${CI_COMMIT_REF_NAME}
|
||||
- docker push registry.gitlab.com/slumber/multi-user/multi-user-server
|
@ -1,6 +1,5 @@
|
||||
pages:
|
||||
stage: doc
|
||||
needs: ["deploy"]
|
||||
image: python
|
||||
script:
|
||||
- pip install -U sphinx sphinx_rtd_theme sphinx-material
|
||||
|
@ -1,7 +0,0 @@
|
||||
test:
|
||||
stage: test
|
||||
image: slumber/blender-addon-testing:latest
|
||||
script:
|
||||
- python3 scripts/test_addon.py
|
||||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "multi_user/libs/replication"]
|
||||
path = multi_user/libs/replication
|
||||
url = https://gitlab.com/slumber/replication.git
|
||||
|
54
CHANGELOG.md
54
CHANGELOG.md
@ -217,3 +217,57 @@ All notable changes to this project will be documented in this file.
|
||||
- GPencil fill stroke
|
||||
- Sculpt and GPencil brushes deleted when joining a session (@Kysios)
|
||||
- Auto-updater doesn't work for master and develop builds
|
||||
|
||||
## [0.5.0] - 2022-02-10
|
||||
|
||||
### Added
|
||||
|
||||
- New overall UI and UX (@Kysios)
|
||||
- Documentation overall update (@Kysios)
|
||||
- Server presets (@Kysios)
|
||||
- Server online status (@Kysios)
|
||||
- Draw connected user color in the user list
|
||||
- Private session (access protected with a password) (@Kysios)
|
||||
|
||||
### Changed
|
||||
|
||||
- Dependencies are now installed in the addon folder and correctly cleaned during the addon removal process
|
||||
|
||||
### Fixed
|
||||
|
||||
- Python 3.10 compatibility (@notfood)
|
||||
- Blender 3.x compatibility
|
||||
- Skin vertex radius synchronization (@kromar)
|
||||
- Sequencer audio strip synchronization
|
||||
- Crash with empty after a reconnection
|
||||
|
||||
## [0.5.1] - 2022-02-10
|
||||
|
||||
### Fixed
|
||||
|
||||
- Auto updater breaks dependency auto installer
|
||||
- Auto updater update from tag
|
||||
|
||||
## [0.5.2] - 2022-02-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- Objects not selectable after user leaves session
|
||||
- Geometry nodes attribute toogle doesn't sync
|
||||
|
||||
## [0.5.3] - 2022-03-11
|
||||
|
||||
### Changed
|
||||
|
||||
- Snapshots logs
|
||||
### Fixed
|
||||
|
||||
- Server crashing during snapshots
|
||||
- Blender 3.1 numpy loading error during early connection process
|
||||
- Server docker arguments
|
||||
|
||||
## [0.5.5] - 2022-06-12
|
||||
|
||||
### Fixed
|
||||
|
||||
- Numpy mesh serialization error
|
@ -16,12 +16,12 @@ import sys
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'Multi-User 0.5.0 Documentation'
|
||||
project = 'Multi-User 0.5.x Documentation'
|
||||
copyright = '2020, Swann Martinez'
|
||||
author = 'Swann Martinez, Poochy, Fabian'
|
||||
|
||||
# The full version, including alpha/beta/rc tags
|
||||
version_release = '0.5.1-develop'
|
||||
version_release = '0.5.5'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
@ -206,20 +206,20 @@ You can run the dedicated server on any platform by following these steps:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python -m pip install replication==0.1.13
|
||||
python -m pip install replication
|
||||
|
||||
4. Launch the server with:
|
||||
3. Launch the server with:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
replication.server
|
||||
replication.serve
|
||||
|
||||
.. hint::
|
||||
You can also specify a custom **port** (-p), **timeout** (-t), **admin password** (-pwd), **log level (ERROR, WARNING, INFO or DEBUG)** (-l) and **log file** (-lf) with the following optional arguments
|
||||
|
||||
.. 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.
|
||||
|
||||
@ -572,7 +572,7 @@ For example, I would like to launch my server with a different administrator pas
|
||||
|
||||
.. 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:
|
||||
|
||||
@ -691,7 +691,7 @@ We're finally ready to launch the server. Simply run:
|
||||
|
||||
.. 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
|
||||
|
||||
|
@ -19,9 +19,9 @@
|
||||
bl_info = {
|
||||
"name": "Multi-User",
|
||||
"author": "Swann Martinez",
|
||||
"version": (0, 4, 0),
|
||||
"version": (0, 6, 0),
|
||||
"description": "Enable real-time collaborative workflow inside blender",
|
||||
"blender": (2, 82, 0),
|
||||
"blender": (4, 0, 0),
|
||||
"location": "3D View > Sidebar > Multi-User tab",
|
||||
"warning": "Unstable addon, use it at your own risks",
|
||||
"category": "Collaboration",
|
||||
@ -43,6 +43,8 @@ from bpy.app.handlers import persistent
|
||||
from . import environment
|
||||
|
||||
|
||||
environment.preload_modules()
|
||||
|
||||
module_error_msg = "Insufficient rights to install the multi-user \
|
||||
dependencies, aunch blender with administrator rights."
|
||||
|
||||
|
@ -1015,9 +1015,11 @@ class Singleton_updater(object):
|
||||
for path, dirs, files in os.walk(base):
|
||||
# prune ie skip updater folder
|
||||
dirs[:] = [d for d in dirs if os.path.join(path,d) not in [self._updater_path]]
|
||||
|
||||
for directory in dirs:
|
||||
shutil.rmtree(os.path.join(path,directory))
|
||||
|
||||
for file in files:
|
||||
for ptrn in self.remove_pre_update_patterns:
|
||||
if fnmatch.filter([file],ptrn):
|
||||
try:
|
||||
fl = os.path.join(path,file)
|
||||
os.remove(fl)
|
||||
@ -1701,7 +1703,7 @@ class GitlabEngine(object):
|
||||
def parse_tags(self, response, updater):
|
||||
if response == None:
|
||||
return []
|
||||
return [{"name": tag["name"], "zipball_url": self.get_zip_url(tag["commit"]["id"], updater)} for tag in response]
|
||||
return [{"name": tag["name"], "zipball_url": f"https://gitlab.com/slumber/multi-user/-/jobs/artifacts/{tag['name']}/download?job=build"} for tag in response]
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
@ -267,7 +267,7 @@ class addon_updater_update_now(bpy.types.Operator):
|
||||
clean_install: bpy.props.BoolProperty(
|
||||
name="Clean install",
|
||||
description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install",
|
||||
default=False,
|
||||
default=True,
|
||||
options={'HIDDEN'}
|
||||
)
|
||||
|
||||
|
@ -43,7 +43,7 @@ __all__ = [
|
||||
"bl_particle",
|
||||
] # Order here defines execution order
|
||||
|
||||
if bpy.app.version[1] >= 91:
|
||||
if bpy.app.version >= (2,91,0):
|
||||
__all__.append('bl_volume')
|
||||
|
||||
from . import *
|
||||
|
@ -53,12 +53,12 @@ STROKE = [
|
||||
"uv_translation",
|
||||
"vertex_color_fill",
|
||||
]
|
||||
if bpy.app.version[1] >= 91:
|
||||
if bpy.app.version >= (2,91,0):
|
||||
STROKE.append('use_cyclic')
|
||||
else:
|
||||
STROKE.append('draw_cyclic')
|
||||
|
||||
if bpy.app.version[1] >= 83:
|
||||
if bpy.app.version >= (2,83,0):
|
||||
STROKE_POINT.append('vertex_color')
|
||||
|
||||
def dump_stroke(stroke):
|
||||
|
@ -37,7 +37,7 @@ class BlLightprobe(ReplicatedDatablock):
|
||||
def construct(data: dict) -> object:
|
||||
type = 'CUBE' if data['type'] == 'CUBEMAP' else data['type']
|
||||
# See https://developer.blender.org/D6396
|
||||
if bpy.app.version[1] >= 83:
|
||||
if bpy.app.version >= (2,83,0):
|
||||
return bpy.data.lightprobes.new(data["name"], type)
|
||||
else:
|
||||
logging.warning("Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
|
||||
@ -49,7 +49,7 @@ class BlLightprobe(ReplicatedDatablock):
|
||||
|
||||
@staticmethod
|
||||
def dump(datablock: object) -> dict:
|
||||
if bpy.app.version[1] < 83:
|
||||
if bpy.app.version < (2,83,0):
|
||||
logging.warning("Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
|
||||
|
||||
dumper = Dumper()
|
||||
|
@ -28,9 +28,14 @@ from replication.protocol import ReplicatedDatablock
|
||||
|
||||
from .bl_datablock import get_datablock_from_uuid, resolve_datablock_from_uuid
|
||||
from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies
|
||||
from bpy.types import (NodeSocketGeometry, NodeSocketShader,
|
||||
NodeSocketVirtual, NodeSocketCollection,
|
||||
NodeSocketObject, NodeSocketMaterial)
|
||||
|
||||
NODE_SOCKET_INDEX = re.compile('\[(\d*)\]')
|
||||
IGNORED_SOCKETS = ['GEOMETRY', 'SHADER', 'CUSTOM']
|
||||
IGNORED_SOCKETS = ['NodeSocketGeometry', 'NodeSocketShader', 'CUSTOM', 'NodeSocketVirtual']
|
||||
IGNORED_SOCKETS_TYPES = (NodeSocketGeometry, NodeSocketShader, NodeSocketVirtual)
|
||||
ID_NODE_SOCKETS = (NodeSocketObject, NodeSocketCollection, NodeSocketMaterial)
|
||||
|
||||
def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree):
|
||||
""" Load a node into a node_tree from a dict
|
||||
@ -57,17 +62,23 @@ def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree):
|
||||
if node_tree_uuid:
|
||||
target_node.node_tree = get_datablock_from_uuid(node_tree_uuid, None)
|
||||
|
||||
if target_node.bl_idname == 'GeometryNodeRepeatOutput':
|
||||
target_node.repeat_items.clear()
|
||||
for sock_name, sock_type in node_data['repeat_items'].items():
|
||||
target_node.repeat_items.new(sock_type, sock_name)
|
||||
|
||||
inputs_data = node_data.get('inputs')
|
||||
if inputs_data:
|
||||
inputs = [i for i in target_node.inputs if i.type not in IGNORED_SOCKETS]
|
||||
inputs = [i for i in target_node.inputs if not isinstance(i, IGNORED_SOCKETS_TYPES)]
|
||||
for idx, inpt in enumerate(inputs):
|
||||
if idx < len(inputs_data) and hasattr(inpt, "default_value"):
|
||||
loaded_input = inputs_data[idx]
|
||||
try:
|
||||
if inpt.type in ['OBJECT', 'COLLECTION']:
|
||||
if isinstance(inpt, ID_NODE_SOCKETS):
|
||||
inpt.default_value = get_datablock_from_uuid(loaded_input, None)
|
||||
else:
|
||||
inpt.default_value = loaded_input
|
||||
setattr(inpt, 'default_value', loaded_input)
|
||||
except Exception as e:
|
||||
logging.warning(f"Node {target_node.name} input {inpt.name} parameter not supported, skipping ({e})")
|
||||
else:
|
||||
@ -75,12 +86,12 @@ def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree):
|
||||
|
||||
outputs_data = node_data.get('outputs')
|
||||
if outputs_data:
|
||||
outputs = [o for o in target_node.outputs if o.type not in IGNORED_SOCKETS]
|
||||
outputs = [o for o in target_node.outputs if not isinstance(o, IGNORED_SOCKETS_TYPES)]
|
||||
for idx, output in enumerate(outputs):
|
||||
if idx < len(outputs_data) and hasattr(output, "default_value"):
|
||||
loaded_output = outputs_data[idx]
|
||||
try:
|
||||
if output.type in ['OBJECT', 'COLLECTION']:
|
||||
if isinstance(output, ID_NODE_SOCKETS):
|
||||
output.default_value = get_datablock_from_uuid(loaded_output, None)
|
||||
else:
|
||||
output.default_value = loaded_output
|
||||
@ -141,7 +152,7 @@ def dump_node(node: bpy.types.ShaderNode) -> dict:
|
||||
|
||||
if hasattr(node, 'inputs'):
|
||||
dumped_node['inputs'] = []
|
||||
inputs = [i for i in node.inputs if i.type not in IGNORED_SOCKETS]
|
||||
inputs = [i for i in node.inputs if not isinstance(i, IGNORED_SOCKETS_TYPES)]
|
||||
for idx, inpt in enumerate(inputs):
|
||||
if hasattr(inpt, 'default_value'):
|
||||
if isinstance(inpt.default_value, bpy.types.ID):
|
||||
@ -154,7 +165,7 @@ def dump_node(node: bpy.types.ShaderNode) -> dict:
|
||||
if hasattr(node, 'outputs'):
|
||||
dumped_node['outputs'] = []
|
||||
for idx, output in enumerate(node.outputs):
|
||||
if output.type not in IGNORED_SOCKETS:
|
||||
if not isinstance(output, IGNORED_SOCKETS_TYPES):
|
||||
if hasattr(output, 'default_value'):
|
||||
dumped_node['outputs'].append(
|
||||
io_dumper.dump(output.default_value))
|
||||
@ -185,6 +196,12 @@ def dump_node(node: bpy.types.ShaderNode) -> dict:
|
||||
dumped_node['image_uuid'] = node.image.uuid
|
||||
if hasattr(node, 'node_tree') and getattr(node, 'node_tree'):
|
||||
dumped_node['node_tree_uuid'] = node.node_tree.uuid
|
||||
|
||||
if node.bl_idname == 'GeometryNodeRepeatInput':
|
||||
dumped_node['paired_output'] = node.paired_output.name
|
||||
|
||||
if node.bl_idname == 'GeometryNodeRepeatOutput':
|
||||
dumped_node['repeat_items'] = {item.name: item.socket_type for item in node.repeat_items}
|
||||
return dumped_node
|
||||
|
||||
|
||||
@ -199,10 +216,8 @@ def load_links(links_data, node_tree):
|
||||
"""
|
||||
|
||||
for link in links_data:
|
||||
input_socket = node_tree.nodes[link['to_node']
|
||||
].inputs[int(link['to_socket'])]
|
||||
output_socket = node_tree.nodes[link['from_node']].outputs[int(
|
||||
link['from_socket'])]
|
||||
input_socket = node_tree.nodes[link['to_node']].inputs[int(link['to_socket'])]
|
||||
output_socket = node_tree.nodes[link['from_node']].outputs[int(link['from_socket'])]
|
||||
node_tree.links.new(input_socket, output_socket)
|
||||
|
||||
|
||||
@ -235,7 +250,7 @@ def dump_node_tree(node_tree: bpy.types.ShaderNodeTree) -> dict:
|
||||
""" Dump a shader node_tree to a dict including links and nodes
|
||||
|
||||
:arg node_tree: dumped shader node tree
|
||||
:type node_tree: bpy.types.ShaderNodeTree
|
||||
:type node_tree: bpy.types.ShaderNodeTree`
|
||||
:return: dict
|
||||
"""
|
||||
node_tree_data = {
|
||||
@ -245,9 +260,8 @@ def dump_node_tree(node_tree: bpy.types.ShaderNodeTree) -> dict:
|
||||
'type': type(node_tree).__name__
|
||||
}
|
||||
|
||||
for socket_id in ['inputs', 'outputs']:
|
||||
socket_collection = getattr(node_tree, socket_id)
|
||||
node_tree_data[socket_id] = dump_node_tree_sockets(socket_collection)
|
||||
sockets = [item for item in node_tree.interface.items_tree if item.item_type == 'SOCKET']
|
||||
node_tree_data['interface'] = dump_node_tree_sockets(sockets)
|
||||
|
||||
return node_tree_data
|
||||
|
||||
@ -263,18 +277,21 @@ def dump_node_tree_sockets(sockets: bpy.types.Collection) -> dict:
|
||||
"""
|
||||
sockets_data = []
|
||||
for socket in sockets:
|
||||
try:
|
||||
socket_uuid = socket['uuid']
|
||||
except Exception:
|
||||
socket_uuid = str(uuid4())
|
||||
socket['uuid'] = socket_uuid
|
||||
|
||||
sockets_data.append((socket.name, socket.bl_socket_idname, socket_uuid))
|
||||
if not socket.socket_type:
|
||||
logging.error(f"Socket {socket.name} has no type, skipping")
|
||||
raise ValueError(f"Socket {socket.name} has no type, skipping")
|
||||
sockets_data.append(
|
||||
(
|
||||
socket.name,
|
||||
socket.socket_type,
|
||||
socket.in_out
|
||||
)
|
||||
)
|
||||
|
||||
return sockets_data
|
||||
|
||||
|
||||
def load_node_tree_sockets(sockets: bpy.types.Collection,
|
||||
def load_node_tree_sockets(interface: bpy.types.NodeTreeInterface,
|
||||
sockets_data: dict):
|
||||
""" load sockets of a shader_node_tree
|
||||
|
||||
@ -285,20 +302,20 @@ def load_node_tree_sockets(sockets: bpy.types.Collection,
|
||||
:arg socket_data: dumped socket data
|
||||
:type socket_data: dict
|
||||
"""
|
||||
# Check for removed sockets
|
||||
for socket in sockets:
|
||||
if not [s for s in sockets_data if 'uuid' in socket and socket['uuid'] == s[2]]:
|
||||
sockets.remove(socket)
|
||||
# Remove old sockets
|
||||
interface.clear()
|
||||
|
||||
# Check for new sockets
|
||||
for idx, socket_data in enumerate(sockets_data):
|
||||
try:
|
||||
checked_socket = sockets[idx]
|
||||
if checked_socket.name != socket_data[0]:
|
||||
checked_socket.name = socket_data[0]
|
||||
except Exception:
|
||||
s = sockets.new(socket_data[1], socket_data[0])
|
||||
s['uuid'] = socket_data[2]
|
||||
for name, socket_type, in_out in sockets_data:
|
||||
if not socket_type:
|
||||
logging.error(f"Socket {name} has no type, skipping")
|
||||
continue
|
||||
socket = interface.new_socket(
|
||||
name,
|
||||
in_out=in_out,
|
||||
socket_type=socket_type
|
||||
)
|
||||
|
||||
|
||||
|
||||
def load_node_tree(node_tree_data: dict, target_node_tree: bpy.types.ShaderNodeTree) -> dict:
|
||||
@ -315,13 +332,8 @@ def load_node_tree(node_tree_data: dict, target_node_tree: bpy.types.ShaderNodeT
|
||||
if not target_node_tree.is_property_readonly('name'):
|
||||
target_node_tree.name = node_tree_data['name']
|
||||
|
||||
if 'inputs' in node_tree_data:
|
||||
socket_collection = getattr(target_node_tree, 'inputs')
|
||||
load_node_tree_sockets(socket_collection, node_tree_data['inputs'])
|
||||
|
||||
if 'outputs' in node_tree_data:
|
||||
socket_collection = getattr(target_node_tree, 'outputs')
|
||||
load_node_tree_sockets(socket_collection, node_tree_data['outputs'])
|
||||
if 'interface' in node_tree_data:
|
||||
load_node_tree_sockets(target_node_tree.interface, node_tree_data['interface'])
|
||||
|
||||
# Load nodes
|
||||
for node in node_tree_data["nodes"]:
|
||||
@ -335,6 +347,15 @@ def load_node_tree(node_tree_data: dict, target_node_tree: bpy.types.ShaderNodeT
|
||||
target_node.parent = target_node_tree.nodes[node_data['parent']]
|
||||
else:
|
||||
target_node.parent = None
|
||||
|
||||
# Load geo node repeat zones
|
||||
zone_input_to_pair = [node_data for node_data in node_tree_data["nodes"].values() if node_data['bl_idname'] == 'GeometryNodeRepeatInput']
|
||||
for node_input_data in zone_input_to_pair:
|
||||
zone_input = target_node_tree.nodes.get(node_input_data['name'])
|
||||
zone_output = target_node_tree.nodes.get(node_input_data['paired_output'])
|
||||
|
||||
zone_input.pair_with_output(zone_output)
|
||||
|
||||
# TODO: load only required nodes links
|
||||
# Load nodes links
|
||||
target_node_tree.links.clear()
|
||||
|
@ -37,8 +37,6 @@ VERTICE = ['co']
|
||||
|
||||
EDGE = [
|
||||
'vertices',
|
||||
'crease',
|
||||
'bevel_weight',
|
||||
'use_seam',
|
||||
'use_edge_sharp',
|
||||
]
|
||||
@ -54,6 +52,18 @@ POLYGON = [
|
||||
'material_index',
|
||||
]
|
||||
|
||||
GENERIC_ATTRIBUTES =[
|
||||
'crease_vert',
|
||||
'crease_edge',
|
||||
'bevel_weight_vert',
|
||||
'bevel_weight_edge'
|
||||
]
|
||||
|
||||
GENERIC_ATTRIBUTES_ENSURE = {
|
||||
'crease_vert': 'vertex_crease_ensure',
|
||||
'crease_edge': 'edge_crease_ensure'
|
||||
}
|
||||
|
||||
class BlMesh(ReplicatedDatablock):
|
||||
use_delta = True
|
||||
|
||||
@ -119,6 +129,16 @@ class BlMesh(ReplicatedDatablock):
|
||||
'color',
|
||||
data["vertex_colors"][color_layer]['data'])
|
||||
|
||||
# Generic attibutes
|
||||
for attribute_name, attribute_data_type, attribute_domain, attribute_data in data["attributes"]:
|
||||
if attribute_name not in datablock.attributes:
|
||||
datablock.attributes.new(
|
||||
attribute_name,
|
||||
attribute_data_type,
|
||||
attribute_domain
|
||||
)
|
||||
np_load_collection(attribute_data, datablock.attributes[attribute_name].data ,['value'])
|
||||
|
||||
datablock.validate()
|
||||
datablock.update()
|
||||
|
||||
@ -135,7 +155,6 @@ class BlMesh(ReplicatedDatablock):
|
||||
'use_auto_smooth',
|
||||
'auto_smooth_angle',
|
||||
'use_customdata_edge_bevel',
|
||||
'use_customdata_edge_crease'
|
||||
]
|
||||
|
||||
data = dumper.dump(mesh)
|
||||
@ -150,6 +169,21 @@ class BlMesh(ReplicatedDatablock):
|
||||
data["egdes_count"] = len(mesh.edges)
|
||||
data["edges"] = np_dump_collection(mesh.edges, EDGE)
|
||||
|
||||
# ATTIBUTES
|
||||
data["attributes"] = []
|
||||
for attribute_name in GENERIC_ATTRIBUTES:
|
||||
if attribute_name in datablock.attributes:
|
||||
attribute_data = datablock.attributes.get(attribute_name)
|
||||
dumped_attr_data = np_dump_collection(attribute_data.data, ['value'])
|
||||
|
||||
data["attributes"].append(
|
||||
(
|
||||
attribute_name,
|
||||
attribute_data.data_type,
|
||||
attribute_data.domain,
|
||||
dumped_attr_data
|
||||
)
|
||||
)
|
||||
# POLYGONS
|
||||
data["poly_count"] = len(mesh.polygons)
|
||||
data["polygons"] = np_dump_collection(mesh.polygons, POLYGON)
|
||||
|
@ -48,7 +48,7 @@ SHAPEKEY_BLOCK_ATTR = [
|
||||
]
|
||||
|
||||
|
||||
if bpy.app.version[1] >= 93:
|
||||
if bpy.app.version >= (2,93,0):
|
||||
SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str, float)
|
||||
else:
|
||||
SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str)
|
||||
@ -56,15 +56,19 @@ else:
|
||||
blender 2.92.")
|
||||
|
||||
|
||||
def get_node_group_inputs(node_group):
|
||||
inputs = []
|
||||
for inpt in node_group.inputs:
|
||||
if inpt.type in IGNORED_SOCKETS:
|
||||
def get_node_group_properties_identifiers(node_group):
|
||||
props_ids = []
|
||||
|
||||
for socket in node_group.interface.items_tree:
|
||||
if socket.socket_type in IGNORED_SOCKETS:
|
||||
continue
|
||||
else:
|
||||
inputs.append(inpt)
|
||||
return inputs
|
||||
# return [inpt.identifer for inpt in node_group.inputs if inpt.type not in IGNORED_SOCKETS]
|
||||
props_ids.append((socket.identifier, socket.socket_type))
|
||||
|
||||
props_ids.append((f"{socket.identifier}_attribute_name",'NodeSocketString'))
|
||||
props_ids.append((f"{socket.identifier}_use_attribute", 'NodeSocketBool'))
|
||||
|
||||
return props_ids
|
||||
|
||||
|
||||
def dump_physics(target: bpy.types.Object)->dict:
|
||||
@ -109,42 +113,51 @@ def load_physics(dumped_settings: dict, target: bpy.types.Object):
|
||||
|
||||
if 'rigid_body' in dumped_settings:
|
||||
if not target.rigid_body:
|
||||
bpy.ops.rigidbody.object_add({"object": target})
|
||||
with bpy.context.temp_override(object=target):
|
||||
bpy.ops.rigidbody.object_add()
|
||||
loader.load(target.rigid_body, dumped_settings['rigid_body'])
|
||||
elif target.rigid_body:
|
||||
bpy.ops.rigidbody.object_remove({"object": target})
|
||||
with bpy.context.temp_override(object=target):
|
||||
bpy.ops.rigidbody.object_remove()
|
||||
|
||||
if 'rigid_body_constraint' in dumped_settings:
|
||||
if not target.rigid_body_constraint:
|
||||
bpy.ops.rigidbody.constraint_add({"object": target})
|
||||
with bpy.context.temp_override(object=target):
|
||||
bpy.ops.rigidbody.constraint_add()
|
||||
loader.load(target.rigid_body_constraint, dumped_settings['rigid_body_constraint'])
|
||||
elif target.rigid_body_constraint:
|
||||
bpy.ops.rigidbody.constraint_remove({"object": target})
|
||||
with bpy.context.temp_override(object=target):
|
||||
bpy.ops.rigidbody.constraint_remove()
|
||||
|
||||
|
||||
def dump_modifier_geometry_node_inputs(modifier: bpy.types.Modifier) -> list:
|
||||
def dump_modifier_geometry_node_props(modifier: bpy.types.Modifier) -> list:
|
||||
""" Dump geometry node modifier input properties
|
||||
|
||||
:arg modifier: geometry node modifier to dump
|
||||
:type modifier: bpy.type.Modifier
|
||||
"""
|
||||
dumped_inputs = []
|
||||
for inpt in get_node_group_inputs(modifier.node_group):
|
||||
input_value = modifier[inpt.identifier]
|
||||
dumped_props = []
|
||||
|
||||
dumped_input = None
|
||||
if isinstance(input_value, bpy.types.ID):
|
||||
dumped_input = input_value.uuid
|
||||
elif isinstance(input_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS):
|
||||
dumped_input = input_value
|
||||
elif hasattr(input_value, 'to_list'):
|
||||
dumped_input = input_value.to_list()
|
||||
dumped_inputs.append(dumped_input)
|
||||
for prop_id, prop_type in get_node_group_properties_identifiers(modifier.node_group):
|
||||
try:
|
||||
prop_value = modifier[prop_id]
|
||||
except KeyError as e:
|
||||
logging.error(f"fail to dump geomety node modifier property : {prop_id} ({e})")
|
||||
else:
|
||||
dump = None
|
||||
if isinstance(prop_value, bpy.types.ID):
|
||||
dump = prop_value.uuid
|
||||
elif isinstance(prop_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS):
|
||||
dump = prop_value
|
||||
elif hasattr(prop_value, 'to_list'):
|
||||
dump = prop_value.to_list()
|
||||
|
||||
return dumped_inputs
|
||||
dumped_props.append((dump, prop_type))
|
||||
|
||||
return dumped_props
|
||||
|
||||
|
||||
def load_modifier_geometry_node_inputs(dumped_modifier: dict, target_modifier: bpy.types.Modifier):
|
||||
def load_modifier_geometry_node_props(dumped_modifier: dict, target_modifier: bpy.types.Modifier):
|
||||
""" Load geometry node modifier inputs
|
||||
|
||||
:arg dumped_modifier: source dumped modifier to load
|
||||
@ -153,17 +166,16 @@ def load_modifier_geometry_node_inputs(dumped_modifier: dict, target_modifier: b
|
||||
:type target_modifier: bpy.type.Modifier
|
||||
"""
|
||||
|
||||
for input_index, inpt in enumerate(get_node_group_inputs(target_modifier.node_group)):
|
||||
dumped_value = dumped_modifier['inputs'][input_index]
|
||||
input_value = target_modifier[inpt.identifier]
|
||||
if isinstance(input_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS):
|
||||
target_modifier[inpt.identifier] = dumped_value
|
||||
elif hasattr(input_value, 'to_list'):
|
||||
for input_index, inpt in enumerate(get_node_group_properties_identifiers(target_modifier.node_group)):
|
||||
dumped_value, dumped_type = dumped_modifier['props'][input_index]
|
||||
input_value = target_modifier[inpt[0]]
|
||||
if dumped_type in ['NodeSocketInt', 'NodeSocketFloat', 'NodeSocketString', 'NodeSocketBool']:
|
||||
target_modifier[inpt[0]] = dumped_value
|
||||
elif dumped_type in ['NodeSocketColor', 'NodeSocketVector']:
|
||||
for index in range(len(input_value)):
|
||||
input_value[index] = dumped_value[index]
|
||||
elif inpt.type in ['COLLECTION', 'OBJECT']:
|
||||
target_modifier[inpt.identifier] = get_datablock_from_uuid(
|
||||
dumped_value, None)
|
||||
elif dumped_type in ['NodeSocketCollection', 'NodeSocketObject', 'NodeSocketImage', 'NodeSocketTexture', 'NodeSocketMaterial']:
|
||||
target_modifier[inpt[0]] = get_datablock_from_uuid(dumped_value, None)
|
||||
|
||||
|
||||
def load_pose(target_bone, data):
|
||||
@ -198,12 +210,12 @@ def find_data_from_name(name=None):
|
||||
instance = bpy.data.speakers[name]
|
||||
elif name in bpy.data.lightprobes.keys():
|
||||
# Only supported since 2.83
|
||||
if bpy.app.version[1] >= 83:
|
||||
if bpy.app.version >= (2,83,0):
|
||||
instance = bpy.data.lightprobes[name]
|
||||
else:
|
||||
logging.warning(
|
||||
"Lightprobe replication only supported since 2.83. See https://developer.blender.org/D6396")
|
||||
elif bpy.app.version[1] >= 91 and name in bpy.data.volumes.keys():
|
||||
elif bpy.app.version >= (2,91,0) and name in bpy.data.volumes.keys():
|
||||
# Only supported since 2.91
|
||||
instance = bpy.data.volumes[name]
|
||||
return instance
|
||||
@ -250,10 +262,11 @@ def find_geometry_nodes_dependencies(modifiers: bpy.types.bpy_prop_collection) -
|
||||
for mod in modifiers:
|
||||
if mod.type == 'NODES' and mod.node_group:
|
||||
dependencies.append(mod.node_group)
|
||||
# for inpt in get_node_group_inputs(mod.node_group):
|
||||
# parameter = mod.get(inpt.identifier)
|
||||
# if parameter and isinstance(parameter, bpy.types.ID):
|
||||
# dependencies.append(parameter)
|
||||
for inpt, inpt_type in get_node_group_properties_identifiers(mod.node_group):
|
||||
inpt_value = mod.get(inpt)
|
||||
# Avoid to handle 'COLLECTION', 'OBJECT' to avoid circular dependencies
|
||||
if inpt_type in ['IMAGE', 'TEXTURE', 'MATERIAL'] and inpt_value:
|
||||
dependencies.append(inpt_value)
|
||||
|
||||
return dependencies
|
||||
|
||||
@ -387,10 +400,7 @@ def dump_modifiers(modifiers: bpy.types.bpy_prop_collection)->dict:
|
||||
dumped_modifier = dumper.dump(modifier)
|
||||
# hack to dump geometry nodes inputs
|
||||
if modifier.type == 'NODES':
|
||||
dumped_inputs = dump_modifier_geometry_node_inputs(
|
||||
modifier)
|
||||
dumped_modifier['inputs'] = dumped_inputs
|
||||
|
||||
dumped_modifier['props'] = dump_modifier_geometry_node_props(modifier)
|
||||
elif modifier.type == 'PARTICLE_SYSTEM':
|
||||
dumper.exclude_filter = [
|
||||
"is_edited",
|
||||
@ -455,7 +465,7 @@ def load_modifiers(dumped_modifiers: list, modifiers: bpy.types.bpy_prop_collect
|
||||
loader.load(loaded_modifier, dumped_modifier)
|
||||
|
||||
if loaded_modifier.type == 'NODES':
|
||||
load_modifier_geometry_node_inputs(dumped_modifier, loaded_modifier)
|
||||
load_modifier_geometry_node_props(dumped_modifier, loaded_modifier)
|
||||
elif loaded_modifier.type == 'PARTICLE_SYSTEM':
|
||||
default = loaded_modifier.particle_system.settings
|
||||
dumped_particles = dumped_modifier['particle_system']
|
||||
@ -565,16 +575,6 @@ class BlObject(ReplicatedDatablock):
|
||||
if 'pose' in data:
|
||||
if not datablock.pose:
|
||||
raise Exception('No pose data yet (Fixed in a near futur)')
|
||||
# Bone groups
|
||||
for bg_name in data['pose']['bone_groups']:
|
||||
bg_data = data['pose']['bone_groups'].get(bg_name)
|
||||
bg_target = datablock.pose.bone_groups.get(bg_name)
|
||||
|
||||
if not bg_target:
|
||||
bg_target = datablock.pose.bone_groups.new(name=bg_name)
|
||||
|
||||
loader.load(bg_target, bg_data)
|
||||
# datablock.pose.bone_groups.get
|
||||
|
||||
# Bones
|
||||
for bone in data['pose']['bones']:
|
||||
@ -586,15 +586,19 @@ class BlObject(ReplicatedDatablock):
|
||||
|
||||
load_pose(target_bone, bone_data)
|
||||
|
||||
if 'bone_index' in bone_data.keys():
|
||||
target_bone.bone_group = datablock.pose.bone_group[bone_data['bone_group_index']]
|
||||
|
||||
# TODO: find another way...
|
||||
if datablock.empty_display_type == "IMAGE":
|
||||
img_uuid = data.get('data_uuid')
|
||||
if datablock.data is None and img_uuid:
|
||||
datablock.data = get_datablock_from_uuid(img_uuid, None)
|
||||
|
||||
if hasattr(datablock, 'cycles_visibility') \
|
||||
and 'cycles_visibility' in data:
|
||||
loader.load(datablock.cycles_visibility, data['cycles_visibility'])
|
||||
|
||||
if hasattr(datablock, 'modifiers'):
|
||||
load_modifiers(data['modifiers'], datablock.modifiers)
|
||||
|
||||
if hasattr(object_data, 'skin_vertices') \
|
||||
and object_data.skin_vertices\
|
||||
and 'skin_vertices' in data:
|
||||
@ -604,13 +608,6 @@ class BlObject(ReplicatedDatablock):
|
||||
skin_data.data,
|
||||
SKIN_DATA)
|
||||
|
||||
if hasattr(datablock, 'cycles_visibility') \
|
||||
and 'cycles_visibility' in data:
|
||||
loader.load(datablock.cycles_visibility, data['cycles_visibility'])
|
||||
|
||||
if hasattr(datablock, 'modifiers'):
|
||||
load_modifiers(data['modifiers'], datablock.modifiers)
|
||||
|
||||
constraints = data.get('constraints')
|
||||
if constraints:
|
||||
load_constraints(constraints, datablock.constraints)
|
||||
@ -728,7 +725,6 @@ class BlObject(ReplicatedDatablock):
|
||||
bones[bone.name] = {}
|
||||
dumper.depth = 1
|
||||
rotation = 'rotation_quaternion' if bone.rotation_mode == 'QUATERNION' else 'rotation_euler'
|
||||
group_index = 'bone_group_index' if bone.bone_group else None
|
||||
dumper.include_filter = [
|
||||
'rotation_mode',
|
||||
'location',
|
||||
@ -736,7 +732,6 @@ class BlObject(ReplicatedDatablock):
|
||||
'custom_shape',
|
||||
'use_custom_shape_bone_size',
|
||||
'custom_shape_scale',
|
||||
group_index,
|
||||
rotation
|
||||
]
|
||||
bones[bone.name] = dumper.dump(bone)
|
||||
@ -747,17 +742,6 @@ class BlObject(ReplicatedDatablock):
|
||||
|
||||
data['pose'] = {'bones': bones}
|
||||
|
||||
# GROUPS
|
||||
bone_groups = {}
|
||||
for group in datablock.pose.bone_groups:
|
||||
dumper.depth = 3
|
||||
dumper.include_filter = [
|
||||
'name',
|
||||
'color_set'
|
||||
]
|
||||
bone_groups[group.name] = dumper.dump(group)
|
||||
data['pose']['bone_groups'] = bone_groups
|
||||
|
||||
# VERTEx GROUP
|
||||
if len(datablock.vertex_groups) > 0:
|
||||
data['vertex_groups'] = dump_vertex_groups(datablock)
|
||||
|
@ -440,7 +440,7 @@ class BlScene(ReplicatedDatablock):
|
||||
if seq.name not in sequences:
|
||||
vse.sequences.remove(seq)
|
||||
# Load existing sequences
|
||||
for seq_data in sequences.value():
|
||||
for seq_data in sequences.values():
|
||||
load_sequence(seq_data, vse)
|
||||
# If the sequence is no longer used, clear it
|
||||
elif datablock.sequence_editor and not sequences:
|
||||
|
@ -26,7 +26,8 @@ import numpy as np
|
||||
BPY_TO_NUMPY_TYPES = {
|
||||
'FLOAT': np.float32,
|
||||
'INT': np.int32,
|
||||
'BOOL': np.bool}
|
||||
'BOOL': bool,
|
||||
'BOOLEAN': bool}
|
||||
|
||||
PRIMITIVE_TYPES = ['FLOAT', 'INT', 'BOOLEAN']
|
||||
|
||||
|
@ -29,15 +29,6 @@ import bpy
|
||||
VERSION_EXPR = re.compile('\d+.\d+.\d+')
|
||||
DEFAULT_CACHE_DIR = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "cache")
|
||||
REPLICATION_DEPENDENCIES = {
|
||||
"zmq",
|
||||
"deepdiff"
|
||||
}
|
||||
LIBS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "libs")
|
||||
REPLICATION = os.path.join(LIBS,"replication")
|
||||
|
||||
PYTHON_PATH = None
|
||||
SUBPROCESS_DIR = None
|
||||
|
||||
rtypes = []
|
||||
|
||||
@ -50,39 +41,20 @@ def module_can_be_imported(name: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def install_pip():
|
||||
def install_pip(python_path):
|
||||
# pip can not necessarily be imported into Blender after this
|
||||
subprocess.run([str(PYTHON_PATH), "-m", "ensurepip"])
|
||||
subprocess.run([str(python_path), "-m", "ensurepip"])
|
||||
|
||||
|
||||
def install_package(name: str, install_dir: str):
|
||||
logging.info(f"installing {name} version...")
|
||||
env = os.environ
|
||||
if "PIP_REQUIRE_VIRTUALENV" in env:
|
||||
# PIP_REQUIRE_VIRTUALENV is an env var to ensure pip cannot install packages outside a virtual env
|
||||
# https://docs.python-guide.org/dev/pip-virtualenv/
|
||||
# But since Blender's pip is outside of a virtual env, it can block our packages installation, so we unset the
|
||||
# env var for the subprocess.
|
||||
env = os.environ.copy()
|
||||
del env["PIP_REQUIRE_VIRTUALENV"]
|
||||
subprocess.run([str(PYTHON_PATH), "-m", "pip", "install", f"{name}", "-t", install_dir], env=env)
|
||||
def preload_modules():
|
||||
from . import wheels
|
||||
|
||||
if name in sys.modules:
|
||||
del sys.modules[name]
|
||||
wheels.load_wheel_global("ordered_set", "ordered_set")
|
||||
wheels.load_wheel_global("deepdiff", "deepdiff")
|
||||
wheels.load_wheel_global("replication", "replication")
|
||||
wheels.load_wheel_global("zmq", "pyzmq", match_platform=True)
|
||||
|
||||
|
||||
def check_package_version(name: str, required_version: str):
|
||||
logging.info(f"Checking {name} version...")
|
||||
out = subprocess.run([str(PYTHON_PATH), "-m", "pip", "show", name], capture_output=True)
|
||||
|
||||
version = VERSION_EXPR.search(out.stdout.decode())
|
||||
if version and version.group() == required_version:
|
||||
logging.info(f"{name} is up to date")
|
||||
return True
|
||||
else:
|
||||
logging.info(f"{name} need an update")
|
||||
return False
|
||||
|
||||
|
||||
def get_ip():
|
||||
"""
|
||||
@ -119,32 +91,8 @@ def remove_paths(paths: list):
|
||||
sys.path.remove(path)
|
||||
|
||||
|
||||
def install_modules(dependencies: list, python_path: str, install_dir: str):
|
||||
global PYTHON_PATH, SUBPROCESS_DIR
|
||||
|
||||
PYTHON_PATH = Path(python_path)
|
||||
SUBPROCESS_DIR = PYTHON_PATH.parent
|
||||
|
||||
if not module_can_be_imported("pip"):
|
||||
install_pip()
|
||||
|
||||
for package_name in dependencies:
|
||||
if not module_can_be_imported(package_name):
|
||||
install_package(package_name, install_dir=install_dir)
|
||||
module_can_be_imported(package_name)
|
||||
|
||||
def register():
|
||||
if bpy.app.version[1] >= 91:
|
||||
python_binary_path = sys.executable
|
||||
else:
|
||||
python_binary_path = bpy.app.binary_path_python
|
||||
|
||||
for module_name in list(sys.modules.keys()):
|
||||
if 'replication' in module_name:
|
||||
del sys.modules[module_name]
|
||||
|
||||
setup_paths([LIBS, REPLICATION])
|
||||
install_modules(REPLICATION_DEPENDENCIES, python_binary_path, install_dir=LIBS)
|
||||
check_dir(DEFAULT_CACHE_DIR)
|
||||
|
||||
def unregister():
|
||||
remove_paths([REPLICATION, LIBS])
|
||||
pass
|
@ -81,9 +81,9 @@ def on_scene_update(scene):
|
||||
|
||||
# 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)
|
||||
update_uuid = getattr(update.id.original, 'uuid', None)
|
||||
if update_uuid:
|
||||
node = session.repository.graph.get(update.id.uuid)
|
||||
node = session.repository.graph.get(update_uuid)
|
||||
check_common = session.repository.rdp.get_implementation(update.id).bl_check_common
|
||||
|
||||
if node and (node.owner == session.repository.username or check_common):
|
||||
|
Submodule multi_user/libs/replication deleted from d69f259046
@ -16,27 +16,21 @@
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
|
||||
import gzip
|
||||
import logging
|
||||
from multi_user.preferences import ServerPreset
|
||||
|
||||
import os
|
||||
import queue
|
||||
import random
|
||||
import shutil
|
||||
import string
|
||||
import sys
|
||||
import time
|
||||
|
||||
import traceback
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
from operator import itemgetter
|
||||
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
from time import gmtime, strftime
|
||||
|
||||
from bpy.props import FloatProperty
|
||||
import bmesh
|
||||
|
||||
try:
|
||||
import _pickle as pickle
|
||||
@ -45,26 +39,135 @@ except ImportError:
|
||||
|
||||
import bpy
|
||||
import mathutils
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||
from replication import porcelain
|
||||
from replication.constants import (COMMITED, FETCHED, RP_COMMON, STATE_ACTIVE,
|
||||
STATE_INITIAL, STATE_SYNCING, UP)
|
||||
from replication.exception import ContextError, NonAuthorizedOperationError
|
||||
from replication.constants import (FETCHED, RP_COMMON, STATE_ACTIVE)
|
||||
from replication.interface import session
|
||||
from replication.objects import Node
|
||||
from replication.protocol import DataTranslationProtocol
|
||||
from replication.repository import Repository
|
||||
|
||||
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, refresh_sidebar_view
|
||||
from .presence import SessionStatusWidget, renderer, view3d_find, refresh_sidebar_view, bbox_from_obj
|
||||
from .timers import registry
|
||||
|
||||
background_execution_queue = Queue()
|
||||
deleyables = []
|
||||
stop_modal_executor = False
|
||||
|
||||
|
||||
def draw_user(username, metadata, radius=0.01, intensity=10.0):
|
||||
view_corners = metadata.get('view_corners')
|
||||
color = metadata.get('color', (1,1,1,0))
|
||||
objects = metadata.get('selected_objects', None)
|
||||
|
||||
user_collection = bpy.data.collections.new(username)
|
||||
|
||||
# User Color
|
||||
user_mat = bpy.data.materials.new(username)
|
||||
user_mat.use_nodes = True
|
||||
nodes = user_mat.node_tree.nodes
|
||||
nodes.remove(nodes['Principled BSDF'])
|
||||
emission_node = nodes.new('ShaderNodeEmission')
|
||||
emission_node.inputs['Color'].default_value = color
|
||||
emission_node.inputs['Strength'].default_value = intensity
|
||||
|
||||
output_node = nodes['Material Output']
|
||||
user_mat.node_tree.links.new(
|
||||
emission_node.outputs['Emission'], output_node.inputs['Surface'])
|
||||
|
||||
# Generate camera mesh
|
||||
camera_vertices = view_corners[:4]
|
||||
camera_vertices.append(view_corners[6])
|
||||
camera_mesh = bpy.data.meshes.new(f"{username}_camera")
|
||||
camera_obj = bpy.data.objects.new(f"{username}_camera", camera_mesh)
|
||||
frustum_bm = bmesh.new()
|
||||
frustum_bm.from_mesh(camera_mesh)
|
||||
|
||||
for p in camera_vertices:
|
||||
frustum_bm.verts.new(p)
|
||||
frustum_bm.verts.ensure_lookup_table()
|
||||
|
||||
frustum_bm.edges.new((frustum_bm.verts[0], frustum_bm.verts[2]))
|
||||
frustum_bm.edges.new((frustum_bm.verts[2], frustum_bm.verts[1]))
|
||||
frustum_bm.edges.new((frustum_bm.verts[1], frustum_bm.verts[3]))
|
||||
frustum_bm.edges.new((frustum_bm.verts[3], frustum_bm.verts[0]))
|
||||
|
||||
frustum_bm.edges.new((frustum_bm.verts[0], frustum_bm.verts[4]))
|
||||
frustum_bm.edges.new((frustum_bm.verts[2], frustum_bm.verts[4]))
|
||||
frustum_bm.edges.new((frustum_bm.verts[1], frustum_bm.verts[4]))
|
||||
frustum_bm.edges.new((frustum_bm.verts[3], frustum_bm.verts[4]))
|
||||
frustum_bm.edges.ensure_lookup_table()
|
||||
|
||||
frustum_bm.to_mesh(camera_mesh)
|
||||
frustum_bm.free() # free and prevent further access
|
||||
|
||||
camera_obj.modifiers.new("wireframe", "SKIN")
|
||||
camera_obj.data.skin_vertices[0].data[0].use_root = True
|
||||
for v in camera_mesh.skin_vertices[0].data:
|
||||
v.radius = [radius, radius]
|
||||
|
||||
camera_mesh.materials.append(user_mat)
|
||||
user_collection.objects.link(camera_obj)
|
||||
|
||||
# Generate sight mesh
|
||||
sight_mesh = bpy.data.meshes.new(f"{username}_sight")
|
||||
sight_obj = bpy.data.objects.new(f"{username}_sight", sight_mesh)
|
||||
sight_verts = view_corners[4:6]
|
||||
sight_bm = bmesh.new()
|
||||
sight_bm.from_mesh(sight_mesh)
|
||||
|
||||
for p in sight_verts:
|
||||
sight_bm.verts.new(p)
|
||||
sight_bm.verts.ensure_lookup_table()
|
||||
|
||||
sight_bm.edges.new((sight_bm.verts[0], sight_bm.verts[1]))
|
||||
sight_bm.edges.ensure_lookup_table()
|
||||
sight_bm.to_mesh(sight_mesh)
|
||||
sight_bm.free()
|
||||
|
||||
sight_obj.modifiers.new("wireframe", "SKIN")
|
||||
sight_obj.data.skin_vertices[0].data[0].use_root = True
|
||||
for v in sight_mesh.skin_vertices[0].data:
|
||||
v.radius = [radius, radius]
|
||||
|
||||
sight_mesh.materials.append(user_mat)
|
||||
user_collection.objects.link(sight_obj)
|
||||
|
||||
# Draw selected objects
|
||||
if objects:
|
||||
for o in list(objects):
|
||||
instance = bl_types.bl_datablock.get_datablock_from_uuid(o, None)
|
||||
if instance:
|
||||
bbox_mesh = bpy.data.meshes.new(f"{instance.name}_bbox")
|
||||
bbox_obj = bpy.data.objects.new(
|
||||
f"{instance.name}_bbox", bbox_mesh)
|
||||
bbox_verts, bbox_ind = bbox_from_obj(instance, index=0)
|
||||
bbox_bm = bmesh.new()
|
||||
bbox_bm.from_mesh(bbox_mesh)
|
||||
|
||||
for p in bbox_verts:
|
||||
bbox_bm.verts.new(p)
|
||||
bbox_bm.verts.ensure_lookup_table()
|
||||
|
||||
for e in bbox_ind:
|
||||
bbox_bm.edges.new(
|
||||
(bbox_bm.verts[e[0]], bbox_bm.verts[e[1]]))
|
||||
|
||||
bbox_bm.to_mesh(bbox_mesh)
|
||||
bbox_bm.free()
|
||||
bpy.data.collections[username].objects.link(bbox_obj)
|
||||
|
||||
bbox_obj.modifiers.new("wireframe", "SKIN")
|
||||
bbox_obj.data.skin_vertices[0].data[0].use_root = True
|
||||
for v in bbox_mesh.skin_vertices[0].data:
|
||||
v.radius = [radius, radius]
|
||||
|
||||
bbox_mesh.materials.append(user_mat)
|
||||
|
||||
bpy.context.scene.collection.children.link(user_collection)
|
||||
|
||||
|
||||
def session_callback(name):
|
||||
""" Session callback wrapper
|
||||
|
||||
@ -136,6 +239,9 @@ def on_connection_end(reason="none"):
|
||||
if on_scene_update in bpy.app.handlers.depsgraph_update_post:
|
||||
bpy.app.handlers.depsgraph_update_post.remove(on_scene_update)
|
||||
|
||||
renderer.clear_widgets()
|
||||
renderer.add_widget("session_status", SessionStatusWidget())
|
||||
|
||||
# Step 3: remove file handled
|
||||
logger = logging.getLogger()
|
||||
for handler in logger.handlers:
|
||||
@ -238,7 +344,7 @@ class SessionConnectOperator(bpy.types.Operator):
|
||||
settings.generate_supported_types()
|
||||
|
||||
|
||||
if bpy.app.version[1] >= 91:
|
||||
if bpy.app.version >= (2,91,0):
|
||||
python_binary_path = sys.executable
|
||||
else:
|
||||
python_binary_path = bpy.app.binary_path_python
|
||||
@ -309,7 +415,7 @@ class SessionHostOperator(bpy.types.Operator):
|
||||
settings.generate_supported_types()
|
||||
|
||||
|
||||
if bpy.app.version[1] >= 91:
|
||||
if bpy.app.version >= (2,91,0):
|
||||
python_binary_path = sys.executable
|
||||
else:
|
||||
python_binary_path = bpy.app.binary_path_python
|
||||
@ -863,6 +969,25 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
|
||||
maxlen=255, # Max internal buffer length, longer would be clamped.
|
||||
)
|
||||
|
||||
draw_users: bpy.props.BoolProperty(
|
||||
name="Load users",
|
||||
description="Draw users in the scene",
|
||||
default=False,
|
||||
)
|
||||
user_skin_radius: bpy.props.FloatProperty(
|
||||
name="Wireframe radius",
|
||||
description="Wireframe radius",
|
||||
default=0.005,
|
||||
)
|
||||
user_color_intensity: bpy.props.FloatProperty(
|
||||
name="Shading intensity",
|
||||
description="Shading intensity",
|
||||
default=10.0,
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
pass
|
||||
|
||||
def execute(self, context):
|
||||
from replication.repository import Repository
|
||||
|
||||
@ -885,6 +1010,17 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
|
||||
for node in nodes:
|
||||
porcelain.apply(repo, node.uuid)
|
||||
|
||||
if self.draw_users:
|
||||
f = gzip.open(self.filepath, "rb")
|
||||
db = pickle.load(f)
|
||||
|
||||
users = db.get("users")
|
||||
|
||||
for username, user_data in users.items():
|
||||
metadata = user_data['metadata']
|
||||
|
||||
if metadata:
|
||||
draw_user(username, metadata, radius=self.user_skin_radius, intensity=self.user_color_intensity)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@ -892,6 +1028,39 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
|
||||
def poll(cls, context):
|
||||
return True
|
||||
|
||||
class SESSION_PT_ImportUser(bpy.types.Panel):
|
||||
bl_space_type = 'FILE_BROWSER'
|
||||
bl_region_type = 'TOOL_PROPS'
|
||||
bl_label = "Users"
|
||||
bl_parent_id = "FILE_PT_operator"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
sfile = context.space_data
|
||||
operator = sfile.active_operator
|
||||
|
||||
return operator.bl_idname == "SESSION_OT_load"
|
||||
|
||||
def draw_header(self, context):
|
||||
sfile = context.space_data
|
||||
operator = sfile.active_operator
|
||||
|
||||
self.layout.prop(operator, "draw_users", text="")
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False # No animation.
|
||||
|
||||
sfile = context.space_data
|
||||
operator = sfile.active_operator
|
||||
|
||||
layout.enabled = operator.draw_users
|
||||
|
||||
layout.prop(operator, "user_skin_radius")
|
||||
layout.prop(operator, "user_color_intensity")
|
||||
|
||||
class SessionPresetServerAdd(bpy.types.Operator):
|
||||
"""Add a server to the server list preset"""
|
||||
bl_idname = "session.preset_server_add"
|
||||
@ -1123,6 +1292,7 @@ classes = (
|
||||
SessionNotifyOperator,
|
||||
SessionSaveBackupOperator,
|
||||
SessionLoadSaveOperator,
|
||||
SESSION_PT_ImportUser,
|
||||
SessionStopAutoSaveOperator,
|
||||
SessionPurgeOperator,
|
||||
SessionPresetServerAdd,
|
||||
|
@ -44,13 +44,6 @@ DEFAULT_PRESETS = {
|
||||
"admin_password": "admin",
|
||||
"server_password": ""
|
||||
},
|
||||
"public session" : {
|
||||
"server_name": "public session",
|
||||
"ip": "51.75.71.183",
|
||||
"port": 5555,
|
||||
"admin_password": "",
|
||||
"server_password": ""
|
||||
},
|
||||
}
|
||||
|
||||
def randomColor():
|
||||
@ -374,9 +367,9 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
||||
description="sidebar_advanced_log_expanded",
|
||||
default=False
|
||||
)
|
||||
sidebar_advanced_hosting_expanded: bpy.props.BoolProperty(
|
||||
name="sidebar_advanced_hosting_expanded",
|
||||
description="sidebar_advanced_hosting_expanded",
|
||||
sidebar_advanced_uinfo_expanded: bpy.props.BoolProperty(
|
||||
name="sidebar_advanced_uinfo_expanded",
|
||||
description="sidebar_advanced_uinfo_expanded",
|
||||
default=False
|
||||
)
|
||||
sidebar_advanced_net_expanded: bpy.props.BoolProperty(
|
||||
@ -619,6 +612,11 @@ class SessionUser(bpy.types.PropertyGroup):
|
||||
"""
|
||||
username: bpy.props.StringProperty(name="username")
|
||||
current_frame: bpy.props.IntProperty(name="current_frame")
|
||||
color: bpy.props.FloatVectorProperty(name="color", subtype="COLOR",
|
||||
min=0.0,
|
||||
max=1.0,
|
||||
size=4,
|
||||
default=(1.0, 1.0, 1.0, 1.0))
|
||||
|
||||
|
||||
class SessionProps(bpy.types.PropertyGroup):
|
||||
|
@ -67,8 +67,10 @@ def refresh_sidebar_view():
|
||||
"""
|
||||
area, region, rv3d = view3d_find()
|
||||
|
||||
if area:
|
||||
area.regions[3].tag_redraw()
|
||||
if area is not None :
|
||||
for region in area.regions:
|
||||
if region.type == "UI":
|
||||
region.tag_redraw()
|
||||
|
||||
|
||||
def project_to_viewport(region: bpy.types.Region, rv3d: bpy.types.RegionView3D, coords: list, distance: float = 1.0) -> list:
|
||||
@ -253,10 +255,9 @@ class Widget(object):
|
||||
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)
|
||||
gpu.state.line_width_set(2.0)
|
||||
gpu.state.depth_test_set("LESS")
|
||||
gpu.state.blend_set("ALPHA")
|
||||
|
||||
|
||||
def draw(self):
|
||||
@ -300,7 +301,8 @@ class UserFrustumWidget(Widget):
|
||||
|
||||
def draw(self):
|
||||
location = self.data.get('view_corners')
|
||||
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
|
||||
shader = gpu.shader.from_builtin('UNIFORM_COLOR')
|
||||
# 'FLAT_COLOR', 'IMAGE', 'IMAGE_COLOR', 'SMOOTH_COLOR', 'UNIFORM_COLOR', 'POLYLINE_FLAT_COLOR', 'POLYLINE_SMOOTH_COLOR', 'POLYLINE_UNIFORM_COLOR'
|
||||
positions = [tuple(coord) for coord in location]
|
||||
|
||||
if len(positions) != 7:
|
||||
@ -372,7 +374,7 @@ class UserSelectionWidget(Widget):
|
||||
vertex_pos += bbox_pos
|
||||
vertex_ind += bbox_ind
|
||||
|
||||
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
|
||||
shader = gpu.shader.from_builtin('UNIFORM_COLOR')
|
||||
batch = batch_for_shader(
|
||||
shader,
|
||||
'LINES',
|
||||
@ -421,7 +423,7 @@ class UserNameWidget(Widget):
|
||||
|
||||
if coords:
|
||||
blf.position(0, coords[0], coords[1]+10, 0)
|
||||
blf.size(0, 16, 72)
|
||||
blf.size(0, 16)
|
||||
blf.color(0, color[0], color[1], color[2], color[3])
|
||||
blf.draw(0, self.username)
|
||||
|
||||
@ -477,7 +479,7 @@ class UserModeWidget(Widget):
|
||||
|
||||
if origin_coord :
|
||||
blf.position(0, origin_coord[0]+8, origin_coord[1]-15, 0)
|
||||
blf.size(0, 16, 72)
|
||||
blf.size(0, 16)
|
||||
blf.color(0, color[0], color[1], color[2], color[3])
|
||||
blf.draw(0, mode_current)
|
||||
|
||||
@ -511,7 +513,7 @@ class SessionStatusWidget(Widget):
|
||||
vpos = (self.preferences.presence_hud_vpos*bpy.context.area.height)/100
|
||||
|
||||
blf.position(0, hpos, vpos, 0)
|
||||
blf.size(0, int(text_scale*ui_scale), 72)
|
||||
blf.size(0, int(text_scale*ui_scale))
|
||||
blf.color(0, color[0], color[1], color[2], color[3])
|
||||
blf.draw(0, state_str)
|
||||
|
||||
|
@ -203,6 +203,7 @@ class DynamicRightSelectTimer(Timer):
|
||||
|
||||
for node_id in to_lock:
|
||||
node = session.repository.graph.get(node_id)
|
||||
if node and hasattr(node,'data'):
|
||||
instance_mode = node.data.get('instance_type')
|
||||
if instance_mode and instance_mode == 'COLLECTION':
|
||||
to_lock.remove(node_id)
|
||||
|
253
multi_user/ui.py
253
multi_user/ui.py
@ -32,6 +32,7 @@ from replication.constants import (ADDED, ERROR, FETCHED,
|
||||
from replication import __version__
|
||||
from replication.interface import session
|
||||
from .timers import registry
|
||||
from . import icons
|
||||
|
||||
ICONS_PROP_STATES = ['TRIA_DOWN', # ADDED
|
||||
'TRIA_UP', # COMMITED
|
||||
@ -62,7 +63,41 @@ def printProgressBar(iteration, total, prefix='', suffix='', decimals=1, length=
|
||||
bar = fill * filledLength + fill_empty * (length - filledLength)
|
||||
return f"{prefix} |{bar}| {iteration}/{total}{suffix}"
|
||||
|
||||
|
||||
def get_mode_icon(mode_name: str) -> str:
|
||||
""" given a mode name retrieve a built-in icon
|
||||
"""
|
||||
mode_icon = "NONE"
|
||||
if mode_name == "OBJECT" :
|
||||
mode_icon = "OBJECT_DATAMODE"
|
||||
elif mode_name == "EDIT_MESH" :
|
||||
mode_icon = "EDITMODE_HLT"
|
||||
elif mode_name == 'EDIT_CURVE':
|
||||
mode_icon = "CURVE_DATA"
|
||||
elif mode_name == 'EDIT_SURFACE':
|
||||
mode_icon = "SURFACE_DATA"
|
||||
elif mode_name == 'EDIT_TEXT':
|
||||
mode_icon = "FILE_FONT"
|
||||
elif mode_name == 'EDIT_ARMATURE':
|
||||
mode_icon = "ARMATURE_DATA"
|
||||
elif mode_name == 'EDIT_METABALL':
|
||||
mode_icon = "META_BALL"
|
||||
elif mode_name == 'EDIT_LATTICE':
|
||||
mode_icon = "LATTICE_DATA"
|
||||
elif mode_name == 'POSE':
|
||||
mode_icon = "POSE_HLT"
|
||||
elif mode_name == 'SCULPT':
|
||||
mode_icon = "SCULPTMODE_HLT"
|
||||
elif mode_name == 'PAINT_WEIGHT':
|
||||
mode_icon = "WPAINT_HLT"
|
||||
elif mode_name == 'PAINT_VERTEX':
|
||||
mode_icon = "VPAINT_HLT"
|
||||
elif mode_name == 'PAINT_TEXTURE':
|
||||
mode_icon = "TPAINT_HLT"
|
||||
elif mode_name == 'PARTICLE':
|
||||
mode_icon = "PARTICLES"
|
||||
elif mode_name == 'PAINT_GPENCIL' or mode_name =='EDIT_GPENCIL' or mode_name =='SCULPT_GPENCIL' or mode_name =='WEIGHT_GPENCIL' or mode_name =='VERTEX_GPENCIL':
|
||||
mode_icon = "GREASEPENCIL"
|
||||
return mode_icon
|
||||
class SESSION_PT_settings(bpy.types.Panel):
|
||||
"""Settings panel"""
|
||||
bl_idname = "MULTIUSER_SETTINGS_PT_panel"
|
||||
@ -75,7 +110,6 @@ class SESSION_PT_settings(bpy.types.Panel):
|
||||
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"]
|
||||
@ -149,10 +183,8 @@ class SESSION_PT_settings(bpy.types.Panel):
|
||||
col.template_list("SESSION_UL_network", "", settings, "server_preset", context.window_manager, "server_index")
|
||||
col.separator()
|
||||
connectOp = col.row()
|
||||
connectOp.operator("session.host", text="Host")
|
||||
connectopcol = connectOp.column()
|
||||
connectopcol.enabled =is_server_selected
|
||||
connectopcol.operator("session.connect", text="Connect")
|
||||
connectOp.enabled =is_server_selected
|
||||
connectOp.operator("session.connect", text="Connect")
|
||||
|
||||
col = row.column(align=True)
|
||||
col.operator("session.preset_server_add", icon="ADD", text="") # TODO : add conditions (need a name, etc..)
|
||||
@ -173,11 +205,17 @@ class SESSION_PT_settings(bpy.types.Panel):
|
||||
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 info_msg:
|
||||
info_box = row.box()
|
||||
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')
|
||||
|
||||
# PROGRESS BAR
|
||||
@ -192,10 +230,57 @@ class SESSION_PT_settings(bpy.types.Panel):
|
||||
length=16
|
||||
))
|
||||
|
||||
class SESSION_PT_host_settings(bpy.types.Panel):
|
||||
bl_idname = "MULTIUSER_SETTINGS_HOST_PT_panel"
|
||||
bl_label = "Hosting"
|
||||
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):
|
||||
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 = "Advanced"
|
||||
bl_label = "General Settings"
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_parent_id = 'MULTIUSER_SETTINGS_PT_panel'
|
||||
@ -216,30 +301,19 @@ class SESSION_PT_advanced_settings(bpy.types.Panel):
|
||||
layout = self.layout
|
||||
settings = get_preferences()
|
||||
|
||||
#ADVANCED HOST
|
||||
host_selection = layout.row().box()
|
||||
host_selection.prop(
|
||||
settings, "sidebar_advanced_hosting_expanded", text="Hosting",
|
||||
icon=get_expanded_icon(settings.sidebar_advanced_hosting_expanded),
|
||||
#ADVANCED USER INFO
|
||||
uinfo_section = layout.row().box()
|
||||
uinfo_section.prop(
|
||||
settings,
|
||||
"sidebar_advanced_uinfo_expanded",
|
||||
text="User Info",
|
||||
icon=get_expanded_icon(settings.sidebar_advanced_uinfo_expanded),
|
||||
emboss=False)
|
||||
if settings.sidebar_advanced_hosting_expanded:
|
||||
host_selection_row = host_selection.row()
|
||||
host_selection_row.prop(settings, "host_port", text="Port:")
|
||||
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_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="")
|
||||
if settings.sidebar_advanced_uinfo_expanded:
|
||||
uinfo_section_row = uinfo_section.row()
|
||||
uinfo_section_split = uinfo_section_row.split(factor=0.7, align=True)
|
||||
uinfo_section_split.prop(settings, "username", text="")
|
||||
uinfo_section_split.prop(settings, "client_color", text="")
|
||||
|
||||
#ADVANCED NET
|
||||
net_section = layout.row().box()
|
||||
@ -335,18 +409,31 @@ class SESSION_PT_user(bpy.types.Panel):
|
||||
online_users)-1 >= selected_user else 0
|
||||
|
||||
#USER LIST
|
||||
row = layout.row()
|
||||
box = row.box()
|
||||
split = box.split(factor=0.35)
|
||||
split.label(text="user")
|
||||
split = split.split(factor=0.3)
|
||||
split.label(text="mode")
|
||||
split.label(text="frame")
|
||||
split.label(text="location")
|
||||
split.label(text="ping")
|
||||
col = layout.column(align=True)
|
||||
row = col.row(align=True)
|
||||
row = row.split(factor=0.35, align=True)
|
||||
|
||||
row = layout.row()
|
||||
layout.template_list("SESSION_UL_users", "", context.window_manager,
|
||||
box = row.box()
|
||||
brow = box.row(align=True)
|
||||
brow.label(text="user")
|
||||
|
||||
row = row.split(factor=0.25, align=True)
|
||||
|
||||
box = row.box()
|
||||
brow = box.row(align=True)
|
||||
brow.label(text="mode")
|
||||
box = row.box()
|
||||
brow = box.row(align=True)
|
||||
brow.label(text="frame")
|
||||
box = row.box()
|
||||
brow = box.row(align=True)
|
||||
brow.label(text="scene")
|
||||
box = row.box()
|
||||
brow = box.row(align=True)
|
||||
brow.label(text="ping")
|
||||
|
||||
row = col.row(align=True)
|
||||
row.template_list("SESSION_UL_users", "", context.window_manager,
|
||||
"online_users", context.window_manager, "user_index")
|
||||
|
||||
#OPERATOR ON USER
|
||||
@ -393,45 +480,32 @@ class SESSION_UL_users(bpy.types.UIList):
|
||||
frame_current = str(metadata.get('frame_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"
|
||||
mode_current = metadata.get('mode_current','-')
|
||||
mode_icon = get_mode_icon(mode_current)
|
||||
user_color = metadata.get('color',[1.0,1.0,1.0,1.0])
|
||||
item.color = user_color
|
||||
if user['admin']:
|
||||
status_icon = 'FAKE_USER_ON'
|
||||
split = layout.split(factor=0.35)
|
||||
split.label(text=item.username, icon=status_icon)
|
||||
split = split.split(factor=0.3)
|
||||
split.label(icon=mode_icon)
|
||||
split.label(text=frame_current)
|
||||
split.label(text=scene_current)
|
||||
split.label(text=ping)
|
||||
row = layout.split(factor=0.35, align=True)
|
||||
entry = row.row(align=True)
|
||||
entry.scale_x = 0.05
|
||||
entry.enabled = False
|
||||
entry.prop(item, 'color', text="", event=False, full_event=False)
|
||||
entry.enabled = True
|
||||
entry.scale_x = 1.0
|
||||
entry.label(icon=status_icon, text="")
|
||||
entry.label(text=item.username)
|
||||
|
||||
row = row.split(factor=0.25, align=True)
|
||||
|
||||
entry = row.row()
|
||||
entry.label(icon=mode_icon)
|
||||
entry = row.row()
|
||||
entry.label(text=frame_current)
|
||||
entry = row.row()
|
||||
entry.label(text=scene_current)
|
||||
entry = row.row()
|
||||
entry.label(text=ping)
|
||||
|
||||
def draw_property(context, parent, property_uuid, level=0):
|
||||
settings = get_preferences()
|
||||
@ -457,7 +531,7 @@ def draw_property(context, parent, property_uuid, level=0):
|
||||
have_right_to_modify = (item.owner == settings.username or \
|
||||
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"]
|
||||
|
||||
@ -543,8 +617,7 @@ class SESSION_PT_repository(bpy.types.Panel):
|
||||
admin = usr['admin']
|
||||
return hasattr(context.window_manager, 'session') and \
|
||||
session and \
|
||||
(session.state == STATE_ACTIVE or \
|
||||
session.state == STATE_LOBBY and admin) and \
|
||||
session.state == STATE_ACTIVE and \
|
||||
not settings.sidebar_repository_shown
|
||||
|
||||
def draw_header(self, context):
|
||||
@ -590,12 +663,6 @@ class SESSION_PT_repository(bpy.types.Panel):
|
||||
else:
|
||||
layout.row().label(text="Empty")
|
||||
|
||||
elif session.state == STATE_LOBBY and usr and usr['admin']:
|
||||
row = layout.row()
|
||||
row.operator("session.init", icon='TOOL_SETTINGS', text="Init")
|
||||
else:
|
||||
row = layout.row()
|
||||
row.label(text="Waiting to start")
|
||||
|
||||
class VIEW3D_PT_overlay_session(bpy.types.Panel):
|
||||
bl_space_type = 'VIEW_3D'
|
||||
@ -614,6 +681,9 @@ class VIEW3D_PT_overlay_session(bpy.types.Panel):
|
||||
pref = get_preferences()
|
||||
layout.active = settings.enable_presence
|
||||
|
||||
row = layout.row()
|
||||
row.prop(settings, "enable_presence",text="Presence Overlay")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(settings, "presence_show_selected",text="Selected Objects")
|
||||
|
||||
@ -657,7 +727,7 @@ class SESSION_UL_network(bpy.types.UIList):
|
||||
else:
|
||||
split.label(text=server_name)
|
||||
|
||||
from multi_user import icons
|
||||
from . import icons
|
||||
server_status = icons.icons_col["server_offline"]
|
||||
if item.is_online:
|
||||
server_status = icons.icons_col["server_online"]
|
||||
@ -667,6 +737,7 @@ classes = (
|
||||
SESSION_UL_users,
|
||||
SESSION_UL_network,
|
||||
SESSION_PT_settings,
|
||||
SESSION_PT_host_settings,
|
||||
SESSION_PT_advanced_settings,
|
||||
SESSION_PT_user,
|
||||
SESSION_PT_sync,
|
||||
|
149
multi_user/wheels/__init__.py
Normal file
149
multi_user/wheels/__init__.py
Normal file
@ -0,0 +1,149 @@
|
||||
"""External dependencies loader."""
|
||||
|
||||
import contextlib
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import logging
|
||||
import sysconfig
|
||||
from types import ModuleType
|
||||
from typing import Iterator, Iterable
|
||||
import zipfile
|
||||
|
||||
|
||||
_my_dir = Path(__file__).parent
|
||||
_log = logging.getLogger(__name__)
|
||||
_env_folder = Path(__file__).parent.joinpath("venv")
|
||||
|
||||
def load_wheel(module_name: str, submodules: Iterable[str]) -> list[ModuleType]:
|
||||
"""Loads modules from a wheel file 'module_name*.whl'.
|
||||
|
||||
Loads `module_name`, and if submodules are given, loads
|
||||
`module_name.submodule` for each of the submodules. This allows loading all
|
||||
required modules from the same wheel in one session, ensuring that
|
||||
inter-submodule references are correct.
|
||||
|
||||
Returns the loaded modules, so [module, submodule, submodule, ...].
|
||||
"""
|
||||
|
||||
fname_prefix = _fname_prefix_from_module_name(module_name)
|
||||
wheel = _wheel_filename(fname_prefix)
|
||||
|
||||
loaded_modules: list[ModuleType] = []
|
||||
to_load = [module_name] + [f"{module_name}.{submodule}" for submodule in submodules]
|
||||
|
||||
# Load the module from the wheel file. Keep a backup of sys.path so that it
|
||||
# can be restored later. This should ensure that future import statements
|
||||
# cannot find this wheel file, increasing the separation of dependencies of
|
||||
# this add-on from other add-ons.
|
||||
with _sys_path_mod_backup(wheel):
|
||||
for modname in to_load:
|
||||
try:
|
||||
module = importlib.import_module(modname)
|
||||
except ImportError as ex:
|
||||
raise ImportError(
|
||||
"Unable to load %r from %s: %s" % (modname, wheel, ex)
|
||||
) from None
|
||||
assert isinstance(module, ModuleType)
|
||||
loaded_modules.append(module)
|
||||
_log.info("Loaded %s from %s", modname, module.__file__)
|
||||
|
||||
assert len(loaded_modules) == len(
|
||||
to_load
|
||||
), f"expecting to load {len(to_load)} modules, but only have {len(loaded_modules)}: {loaded_modules}"
|
||||
return loaded_modules
|
||||
|
||||
|
||||
def load_wheel_global(module_name: str, fname_prefix: str = "", match_platform: bool = False) -> ModuleType:
|
||||
"""Loads a wheel from 'fname_prefix*.whl', unless the named module can be imported.
|
||||
|
||||
This allows us to use system-installed packages before falling back to the shipped wheels.
|
||||
This is useful for development, less so for deployment.
|
||||
|
||||
If `fname_prefix` is the empty string, it will use the first package from `module_name`.
|
||||
In other words, `module_name="pkg.subpkg"` will result in `fname_prefix="pkg"`.
|
||||
"""
|
||||
|
||||
if not fname_prefix:
|
||||
fname_prefix = _fname_prefix_from_module_name(module_name)
|
||||
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
except ImportError as ex:
|
||||
_log.debug("Unable to import %s directly, will try wheel: %s", module_name, ex)
|
||||
else:
|
||||
_log.debug(
|
||||
"Was able to load %s from %s, no need to load wheel %s",
|
||||
module_name,
|
||||
module.__file__,
|
||||
fname_prefix,
|
||||
)
|
||||
return module
|
||||
|
||||
wheel = _wheel_filename(fname_prefix, match_platform=match_platform)
|
||||
|
||||
wheel_filepath = str(wheel)
|
||||
|
||||
|
||||
wheel_archive = zipfile.ZipFile(wheel_filepath)
|
||||
|
||||
|
||||
wheel_archive.extractall(_env_folder)
|
||||
|
||||
if str(_env_folder) not in sys.path:
|
||||
sys.path.insert(0, str(_env_folder))
|
||||
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
except ImportError as ex:
|
||||
raise ImportError(
|
||||
"Unable to load %r from %s: %s" % (module_name, wheel, ex)
|
||||
) from None
|
||||
|
||||
_log.debug("Globally loaded %s from %s", module_name, module.__file__)
|
||||
return module
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _sys_path_mod_backup(wheel_file: Path) -> Iterator[None]:
|
||||
"""Temporarily inserts a wheel onto sys.path.
|
||||
|
||||
When the context exits, it restores sys.path and sys.modules, so that
|
||||
anything that was imported within the context remains unimportable by other
|
||||
modules.
|
||||
"""
|
||||
old_syspath = sys.path[:]
|
||||
old_sysmod = sys.modules.copy()
|
||||
|
||||
try:
|
||||
sys.path.insert(0, str(wheel_file))
|
||||
yield
|
||||
finally:
|
||||
# Restore without assigning a new list instance. That way references
|
||||
# held by other code will stay valid.
|
||||
sys.path[:] = old_syspath
|
||||
sys.modules.clear()
|
||||
sys.modules.update(old_sysmod)
|
||||
|
||||
|
||||
def _wheel_filename(fname_prefix: str, match_platform: bool = False) -> Path:
|
||||
if match_platform:
|
||||
platform_tag = sysconfig.get_platform().replace('-','_').replace('.','_')
|
||||
path_pattern = f"{fname_prefix}*{platform_tag}.whl"
|
||||
else:
|
||||
path_pattern = f"{fname_prefix}*.whl"
|
||||
wheels: list[Path] = list(_my_dir.glob(path_pattern))
|
||||
if not wheels:
|
||||
raise RuntimeError("Unable to find wheel at %r" % path_pattern)
|
||||
|
||||
# If there are multiple wheels that match, load the last-modified one.
|
||||
# Alphabetical sorting isn't going to cut it since BAT 1.10 was released.
|
||||
def modtime(filepath: Path) -> float:
|
||||
return filepath.stat().st_mtime
|
||||
|
||||
wheels.sort(key=modtime)
|
||||
return wheels[-1]
|
||||
|
||||
|
||||
def _fname_prefix_from_module_name(module_name: str) -> str:
|
||||
return module_name.split(".", 1)[0]
|
BIN
multi_user/wheels/deepdiff-5.7.0-py3-none-any.whl
Normal file
BIN
multi_user/wheels/deepdiff-5.7.0-py3-none-any.whl
Normal file
Binary file not shown.
BIN
multi_user/wheels/ordered_set-4.1.0-py3-none-any.whl
Normal file
BIN
multi_user/wheels/ordered_set-4.1.0-py3-none-any.whl
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
multi_user/wheels/pyzmq-25.1.2-cp310-cp310-win32.whl
Normal file
BIN
multi_user/wheels/pyzmq-25.1.2-cp310-cp310-win32.whl
Normal file
Binary file not shown.
BIN
multi_user/wheels/pyzmq-25.1.2-cp310-cp310-win_amd64.whl
Normal file
BIN
multi_user/wheels/pyzmq-25.1.2-cp310-cp310-win_amd64.whl
Normal file
Binary file not shown.
BIN
multi_user/wheels/replication-0.9.9-py3-none-any.whl
Normal file
BIN
multi_user/wheels/replication-0.9.9-py3-none-any.whl
Normal file
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
# Download base image debian jessie
|
||||
FROM python:slim
|
||||
|
||||
ARG replication_version=0.1.13
|
||||
ARG replication_version=0.9.1
|
||||
ARG version=0.1.1
|
||||
|
||||
# Infos
|
||||
@ -22,4 +22,4 @@ RUN pip install replication==$replication_version
|
||||
|
||||
# Run the server with parameters
|
||||
ENTRYPOINT ["/bin/sh", "-c"]
|
||||
CMD ["python3 -m replication.server -pwd ${password} -p ${port} -t ${timeout} -l ${log_level} -lf ${log_file}"]
|
||||
CMD ["replication.server -apwd ${password} -spwd '' -p ${port} -t ${timeout} -l ${log_level} -lf ${log_file}"]
|
@ -7,7 +7,7 @@ import bpy
|
||||
from multi_user.bl_types.bl_lightprobe import BlLightprobe
|
||||
|
||||
|
||||
@pytest.mark.skipif(bpy.app.version[1] < 83, reason="requires blender 2.83 or higher")
|
||||
@pytest.mark.skipif(bpy.app.version < (2,83,0), reason="requires blender 2.83 or higher")
|
||||
@pytest.mark.parametrize('lightprobe_type', ['PLANAR','GRID','CUBEMAP'])
|
||||
def test_lightprobes(clear_blend, lightprobe_type):
|
||||
bpy.ops.object.lightprobe_add(type=lightprobe_type)
|
||||
|
Reference in New Issue
Block a user