Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1896994418 | |||
| ded9592317 | |||
| 9db30877b9 | |||
| 87c2cecf14 | |||
| cdbf59619e | |||
| 5b2211d95b |
@@ -5,7 +5,6 @@ stages:
|
|||||||
- doc
|
- doc
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
include:
|
include:
|
||||||
- local: .gitlab/ci/test.gitlab-ci.yml
|
- local: .gitlab/ci/test.gitlab-ci.yml
|
||||||
- local: .gitlab/ci/build.gitlab-ci.yml
|
- local: .gitlab/ci/build.gitlab-ci.yml
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
build:
|
build:
|
||||||
stage: build
|
stage: build
|
||||||
needs: ["test"]
|
|
||||||
image: debian:stable-slim
|
image: debian:stable-slim
|
||||||
script:
|
script:
|
||||||
- rm -rf tests .git .gitignore script
|
- rm -rf tests .git .gitignore script
|
||||||
@@ -8,3 +7,8 @@ build:
|
|||||||
name: multi_user
|
name: multi_user
|
||||||
paths:
|
paths:
|
||||||
- multi_user
|
- multi_user
|
||||||
|
only:
|
||||||
|
refs:
|
||||||
|
- master
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
deploy:
|
deploy:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
needs: ["build"]
|
|
||||||
image: slumber/docker-python
|
image: slumber/docker-python
|
||||||
variables:
|
variables:
|
||||||
DOCKER_DRIVER: overlay2
|
DOCKER_DRIVER: overlay2
|
||||||
@@ -18,3 +17,7 @@ deploy:
|
|||||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
- 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 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
|
- docker push registry.gitlab.com/slumber/multi-user/multi-user-server
|
||||||
|
only:
|
||||||
|
refs:
|
||||||
|
- master
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
pages:
|
pages:
|
||||||
stage: doc
|
stage: doc
|
||||||
needs: ["deploy"]
|
image: python:3.7-alpine
|
||||||
image: python
|
|
||||||
script:
|
script:
|
||||||
- pip install -U sphinx sphinx_rtd_theme sphinx-material
|
- pip install -U sphinx sphinx_rtd_theme
|
||||||
- sphinx-build -b html ./docs public
|
- sphinx-build -b html ./docs public
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- public
|
- public
|
||||||
only:
|
|
||||||
refs:
|
|
||||||
- master
|
|
||||||
- develop
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,3 +3,6 @@ test:
|
|||||||
image: slumber/blender-addon-testing:latest
|
image: slumber/blender-addon-testing:latest
|
||||||
script:
|
script:
|
||||||
- python3 scripts/test_addon.py
|
- python3 scripts/test_addon.py
|
||||||
|
only:
|
||||||
|
refs:
|
||||||
|
- master
|
||||||
|
|||||||
@@ -19,46 +19,44 @@ This tool aims to allow multiple users to work on the same scene over the networ
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
See the [documentation](https://slumber.gitlab.io/multi-user/index.html) for details.
|
See the [documentation](https://multi-user.readthedocs.io/en/latest/) for details.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
See the [troubleshooting guide](https://slumber.gitlab.io/multi-user/getting_started/troubleshooting.html) for tips on the most common issues.
|
See the [troubleshooting guide](https://multi-user.readthedocs.io/en/latest/getting_started/troubleshooting.html) for tips on the most common issues.
|
||||||
|
|
||||||
## Current development status
|
## Current development status
|
||||||
|
|
||||||
Currently, not all data-block are supported for replication over the wire. The following list summarizes the status for each ones.
|
Currently, not all data-block are supported for replication over the wire. The following list summarizes the status for each ones.
|
||||||
|
|
||||||
| Name | Status | Comment |
|
| Name | Status | Comment |
|
||||||
| -------------- | :----: | :--------------------------------------------------------------------------: |
|
| ----------- | :----: | :--------------------------------------------------------------------------: |
|
||||||
| action | ✔️ | |
|
| action | ✔️ | |
|
||||||
| armature | ❗ | Not stable |
|
| armature | ❗ | Not stable |
|
||||||
| camera | ✔️ | |
|
| camera | ✔️ | |
|
||||||
| collection | ✔️ | |
|
| collection | ✔️ | |
|
||||||
| curve | ❗ | Nurbs surfaces not supported |
|
| curve | ❗ | Nurbs not supported |
|
||||||
| gpencil | ✔️ | [Airbrush not supported](https://gitlab.com/slumber/multi-user/-/issues/123) |
|
| gpencil | ✔️ | [Airbrush not supported](https://gitlab.com/slumber/multi-user/-/issues/123) |
|
||||||
| image | ✔️ | |
|
| image | ✔️ | |
|
||||||
| mesh | ✔️ | |
|
| mesh | ✔️ | |
|
||||||
| material | ✔️ | |
|
| material | ✔️ | |
|
||||||
| node_groups | ❗ | Material only |
|
| node_groups | ❗ | Material only |
|
||||||
| geometry nodes | ✔️ | |
|
| metaball | ✔️ | |
|
||||||
| metaball | ✔️ | |
|
| object | ✔️ | |
|
||||||
| object | ✔️ | |
|
| textures | ❗ | Supported for modifiers only |
|
||||||
| textures | ❗ | Supported for modifiers/materials only |
|
| texts | ✔️ | |
|
||||||
| texts | ✔️ | |
|
| scene | ✔️ | |
|
||||||
| scene | ✔️ | |
|
| world | ✔️ | |
|
||||||
| world | ✔️ | |
|
| lightprobes | ✔️ | |
|
||||||
| lightprobes | ✔️ | |
|
| compositing | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/46) |
|
||||||
| compositing | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/46) |
|
| texts | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/81) |
|
||||||
| texts | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/81) |
|
| nla | ❌ | |
|
||||||
| nla | ❌ | |
|
| volumes | ✔️ | |
|
||||||
| volumes | ✔️ | |
|
| particles | ❌ | [On-going](https://gitlab.com/slumber/multi-user/-/issues/24) |
|
||||||
| particles | ❌ | [On-going](https://gitlab.com/slumber/multi-user/-/issues/24) |
|
| 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) |
|
||||||
| physics | ❌ | [Planned](https://gitlab.com/slumber/multi-user/-/issues/45) |
|
| libraries | ❗ | Partial |
|
||||||
| libraries | ❗ | Partial |
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Performance issues
|
### Performance issues
|
||||||
@@ -70,13 +68,13 @@ I'm working on it.
|
|||||||
|
|
||||||
| Dependencies | Version | Needed |
|
| Dependencies | Version | Needed |
|
||||||
| ------------ | :-----: | -----: |
|
| ------------ | :-----: | -----: |
|
||||||
| Replication | latest | yes |
|
| Replication | latest | yes |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
See [contributing section](https://slumber.gitlab.io/multi-user/ways_to_contribute.html) of the documentation.
|
See [contributing section](https://multi-user.readthedocs.io/en/latest/ways_to_contribute.html) of the documentation.
|
||||||
|
|
||||||
Feel free to [join the discord server](https://discord.gg/aBPvGws) to chat, seek help and contribute.
|
Feel free to [join the discord server](https://discord.gg/aBPvGws) to chat, seek help and contribute.
|
||||||
|
|
||||||
|
|||||||
+102
-129
@@ -27,7 +27,7 @@ from .dump_anything import Loader, Dumper
|
|||||||
from .bl_datablock import BlDatablock, get_datablock_from_uuid
|
from .bl_datablock import BlDatablock, get_datablock_from_uuid
|
||||||
|
|
||||||
NODE_SOCKET_INDEX = re.compile('\[(\d*)\]')
|
NODE_SOCKET_INDEX = re.compile('\[(\d*)\]')
|
||||||
IGNORED_SOCKETS = ['GEOMETRY', 'SHADER', 'CUSTOM']
|
IGNORED_SOCKETS = ['GEOMETRY']
|
||||||
|
|
||||||
def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree):
|
def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree):
|
||||||
""" Load a node into a node_tree from a dict
|
""" Load a node into a node_tree from a dict
|
||||||
@@ -53,136 +53,31 @@ def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree):
|
|||||||
inputs_data = node_data.get('inputs')
|
inputs_data = node_data.get('inputs')
|
||||||
if inputs_data:
|
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 i.type not in IGNORED_SOCKETS]
|
||||||
for idx, inpt in enumerate(inputs):
|
|
||||||
if idx < len(inputs_data) and hasattr(inpt, "default_value"):
|
for idx, inpt in enumerate(inputs_data):
|
||||||
loaded_input = inputs_data[idx]
|
if idx < len(inputs) and hasattr(inputs[idx], "default_value"):
|
||||||
try:
|
try:
|
||||||
if inpt.type in ['OBJECT', 'COLLECTION']:
|
inputs[idx].default_value = inpt
|
||||||
inpt.default_value = get_datablock_from_uuid(loaded_input, None)
|
|
||||||
else:
|
|
||||||
inpt.default_value = loaded_input
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(f"Node {target_node.name} input {inpt.name} parameter not supported, skipping ({e})")
|
logging.warning(f"Node {target_node.name} input {inputs[idx].name} parameter not supported, skipping ({e})")
|
||||||
else:
|
else:
|
||||||
logging.warning(f"Node {target_node.name} input length mismatch.")
|
logging.warning(f"Node {target_node.name} input length mismatch.")
|
||||||
|
|
||||||
outputs_data = node_data.get('outputs')
|
outputs_data = node_data.get('outputs')
|
||||||
if outputs_data:
|
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 o.type not in IGNORED_SOCKETS]
|
||||||
for idx, output in enumerate(outputs):
|
for idx, output in enumerate(outputs_data):
|
||||||
if idx < len(outputs_data) and hasattr(output, "default_value"):
|
if idx < len(outputs) and hasattr(outputs[idx], "default_value"):
|
||||||
loaded_output = outputs_data[idx]
|
|
||||||
try:
|
try:
|
||||||
if output.type in ['OBJECT', 'COLLECTION']:
|
outputs[idx].default_value = output
|
||||||
output.default_value = get_datablock_from_uuid(loaded_output, None)
|
|
||||||
else:
|
|
||||||
output.default_value = loaded_output
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
f"Node {target_node.name} output {output.name} parameter not supported, skipping ({e})")
|
f"Node {target_node.name} output {outputs[idx].name} parameter not supported, skipping ({e})")
|
||||||
else:
|
else:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
f"Node {target_node.name} output length mismatch.")
|
f"Node {target_node.name} output length mismatch.")
|
||||||
|
|
||||||
|
|
||||||
def dump_node(node: bpy.types.ShaderNode) -> dict:
|
|
||||||
""" Dump a single node to a dict
|
|
||||||
|
|
||||||
:arg node: target node
|
|
||||||
:type node: bpy.types.Node
|
|
||||||
:retrun: dict
|
|
||||||
"""
|
|
||||||
|
|
||||||
node_dumper = Dumper()
|
|
||||||
node_dumper.depth = 1
|
|
||||||
node_dumper.exclude_filter = [
|
|
||||||
"dimensions",
|
|
||||||
"show_expanded",
|
|
||||||
"name_full",
|
|
||||||
"select",
|
|
||||||
"bl_label",
|
|
||||||
"bl_height_min",
|
|
||||||
"bl_height_max",
|
|
||||||
"bl_height_default",
|
|
||||||
"bl_width_min",
|
|
||||||
"bl_width_max",
|
|
||||||
"type",
|
|
||||||
"bl_icon",
|
|
||||||
"bl_width_default",
|
|
||||||
"bl_static_type",
|
|
||||||
"show_tetxure",
|
|
||||||
"is_active_output",
|
|
||||||
"hide",
|
|
||||||
"show_options",
|
|
||||||
"show_preview",
|
|
||||||
"show_texture",
|
|
||||||
"outputs",
|
|
||||||
"width_hidden",
|
|
||||||
"image"
|
|
||||||
]
|
|
||||||
|
|
||||||
dumped_node = node_dumper.dump(node)
|
|
||||||
|
|
||||||
if node.parent:
|
|
||||||
dumped_node['parent'] = node.parent.name
|
|
||||||
|
|
||||||
dump_io_needed = (node.type not in ['REROUTE', 'OUTPUT_MATERIAL'])
|
|
||||||
|
|
||||||
if dump_io_needed:
|
|
||||||
io_dumper = Dumper()
|
|
||||||
io_dumper.depth = 2
|
|
||||||
io_dumper.include_filter = ["default_value"]
|
|
||||||
|
|
||||||
if hasattr(node, 'inputs'):
|
|
||||||
dumped_node['inputs'] = []
|
|
||||||
inputs = [i for i in node.inputs if i.type not in IGNORED_SOCKETS]
|
|
||||||
for idx, inpt in enumerate(inputs):
|
|
||||||
if hasattr(inpt, 'default_value'):
|
|
||||||
if isinstance(inpt.default_value, bpy.types.ID):
|
|
||||||
dumped_input = inpt.default_value.uuid
|
|
||||||
else:
|
|
||||||
dumped_input = io_dumper.dump(inpt.default_value)
|
|
||||||
|
|
||||||
dumped_node['inputs'].append(dumped_input)
|
|
||||||
|
|
||||||
if hasattr(node, 'outputs'):
|
|
||||||
dumped_node['outputs'] = []
|
|
||||||
for idx, output in enumerate(node.outputs):
|
|
||||||
if output.type not in IGNORED_SOCKETS:
|
|
||||||
if hasattr(output, 'default_value'):
|
|
||||||
dumped_node['outputs'].append(
|
|
||||||
io_dumper.dump(output.default_value))
|
|
||||||
|
|
||||||
if hasattr(node, 'color_ramp'):
|
|
||||||
ramp_dumper = Dumper()
|
|
||||||
ramp_dumper.depth = 4
|
|
||||||
ramp_dumper.include_filter = [
|
|
||||||
'elements',
|
|
||||||
'alpha',
|
|
||||||
'color',
|
|
||||||
'position',
|
|
||||||
'interpolation',
|
|
||||||
'hue_interpolation',
|
|
||||||
'color_mode'
|
|
||||||
]
|
|
||||||
dumped_node['color_ramp'] = ramp_dumper.dump(node.color_ramp)
|
|
||||||
if hasattr(node, 'mapping'):
|
|
||||||
curve_dumper = Dumper()
|
|
||||||
curve_dumper.depth = 5
|
|
||||||
curve_dumper.include_filter = [
|
|
||||||
'curves',
|
|
||||||
'points',
|
|
||||||
'location'
|
|
||||||
]
|
|
||||||
dumped_node['mapping'] = curve_dumper.dump(node.mapping)
|
|
||||||
if hasattr(node, 'image') and getattr(node, 'image'):
|
|
||||||
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
|
|
||||||
return dumped_node
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def load_links(links_data, node_tree):
|
def load_links(links_data, node_tree):
|
||||||
""" Load node_tree links from a list
|
""" Load node_tree links from a list
|
||||||
|
|
||||||
@@ -225,6 +120,93 @@ def dump_links(links):
|
|||||||
return links_data
|
return links_data
|
||||||
|
|
||||||
|
|
||||||
|
def dump_node(node: bpy.types.ShaderNode) -> dict:
|
||||||
|
""" Dump a single node to a dict
|
||||||
|
|
||||||
|
:arg node: target node
|
||||||
|
:type node: bpy.types.Node
|
||||||
|
:retrun: dict
|
||||||
|
"""
|
||||||
|
|
||||||
|
node_dumper = Dumper()
|
||||||
|
node_dumper.depth = 1
|
||||||
|
node_dumper.exclude_filter = [
|
||||||
|
"dimensions",
|
||||||
|
"show_expanded",
|
||||||
|
"name_full",
|
||||||
|
"select",
|
||||||
|
"bl_label",
|
||||||
|
"bl_height_min",
|
||||||
|
"bl_height_max",
|
||||||
|
"bl_height_default",
|
||||||
|
"bl_width_min",
|
||||||
|
"bl_width_max",
|
||||||
|
"type",
|
||||||
|
"bl_icon",
|
||||||
|
"bl_width_default",
|
||||||
|
"bl_static_type",
|
||||||
|
"show_tetxure",
|
||||||
|
"is_active_output",
|
||||||
|
"hide",
|
||||||
|
"show_options",
|
||||||
|
"show_preview",
|
||||||
|
"show_texture",
|
||||||
|
"outputs",
|
||||||
|
"width_hidden",
|
||||||
|
"image"
|
||||||
|
]
|
||||||
|
|
||||||
|
dumped_node = node_dumper.dump(node)
|
||||||
|
|
||||||
|
dump_io_needed = (node.type not in ['REROUTE', 'OUTPUT_MATERIAL'])
|
||||||
|
|
||||||
|
if dump_io_needed:
|
||||||
|
io_dumper = Dumper()
|
||||||
|
io_dumper.depth = 2
|
||||||
|
io_dumper.include_filter = ["default_value"]
|
||||||
|
|
||||||
|
if hasattr(node, 'inputs'):
|
||||||
|
dumped_node['inputs'] = []
|
||||||
|
for idx, inpt in enumerate(node.inputs):
|
||||||
|
if hasattr(inpt, 'default_value'):
|
||||||
|
dumped_node['inputs'].append(
|
||||||
|
io_dumper.dump(inpt.default_value))
|
||||||
|
|
||||||
|
if hasattr(node, 'outputs'):
|
||||||
|
dumped_node['outputs'] = []
|
||||||
|
for idx, output in enumerate(node.outputs):
|
||||||
|
if hasattr(output, 'default_value'):
|
||||||
|
dumped_node['outputs'].append(
|
||||||
|
io_dumper.dump(output.default_value))
|
||||||
|
|
||||||
|
if hasattr(node, 'color_ramp'):
|
||||||
|
ramp_dumper = Dumper()
|
||||||
|
ramp_dumper.depth = 4
|
||||||
|
ramp_dumper.include_filter = [
|
||||||
|
'elements',
|
||||||
|
'alpha',
|
||||||
|
'color',
|
||||||
|
'position',
|
||||||
|
'interpolation',
|
||||||
|
'color_mode'
|
||||||
|
]
|
||||||
|
dumped_node['color_ramp'] = ramp_dumper.dump(node.color_ramp)
|
||||||
|
if hasattr(node, 'mapping'):
|
||||||
|
curve_dumper = Dumper()
|
||||||
|
curve_dumper.depth = 5
|
||||||
|
curve_dumper.include_filter = [
|
||||||
|
'curves',
|
||||||
|
'points',
|
||||||
|
'location'
|
||||||
|
]
|
||||||
|
dumped_node['mapping'] = curve_dumper.dump(node.mapping)
|
||||||
|
if hasattr(node, 'image') and getattr(node, 'image'):
|
||||||
|
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
|
||||||
|
return dumped_node
|
||||||
|
|
||||||
|
|
||||||
def dump_node_tree(node_tree: bpy.types.ShaderNodeTree) -> dict:
|
def dump_node_tree(node_tree: bpy.types.ShaderNodeTree) -> dict:
|
||||||
""" Dump a shader node_tree to a dict including links and nodes
|
""" Dump a shader node_tree to a dict including links and nodes
|
||||||
|
|
||||||
@@ -281,7 +263,7 @@ def load_node_tree_sockets(sockets: bpy.types.Collection,
|
|||||||
"""
|
"""
|
||||||
# Check for removed sockets
|
# Check for removed sockets
|
||||||
for socket in sockets:
|
for socket in sockets:
|
||||||
if not [s for s in sockets_data if 'uuid' in socket and socket['uuid'] == s[2]]:
|
if not [s for s in sockets_data if socket['uuid'] == s[2]]:
|
||||||
sockets.remove(socket)
|
sockets.remove(socket)
|
||||||
|
|
||||||
# Check for new sockets
|
# Check for new sockets
|
||||||
@@ -321,14 +303,6 @@ def load_node_tree(node_tree_data: dict, target_node_tree: bpy.types.ShaderNodeT
|
|||||||
for node in node_tree_data["nodes"]:
|
for node in node_tree_data["nodes"]:
|
||||||
load_node(node_tree_data["nodes"][node], target_node_tree)
|
load_node(node_tree_data["nodes"][node], target_node_tree)
|
||||||
|
|
||||||
for node_id, node_data in node_tree_data["nodes"].items():
|
|
||||||
target_node = target_node_tree.nodes.get(node_id, None)
|
|
||||||
if target_node is None:
|
|
||||||
continue
|
|
||||||
elif 'parent' in node_data:
|
|
||||||
target_node.parent = target_node_tree.nodes[node_data['parent']]
|
|
||||||
else:
|
|
||||||
target_node.parent = None
|
|
||||||
# TODO: load only required nodes links
|
# TODO: load only required nodes links
|
||||||
# Load nodes links
|
# Load nodes links
|
||||||
target_node_tree.links.clear()
|
target_node_tree.links.clear()
|
||||||
@@ -343,8 +317,6 @@ def get_node_tree_dependencies(node_tree: bpy.types.NodeTree) -> list:
|
|||||||
def has_node_group(node): return (
|
def has_node_group(node): return (
|
||||||
hasattr(node, 'node_tree') and node.node_tree)
|
hasattr(node, 'node_tree') and node.node_tree)
|
||||||
|
|
||||||
def has_texture(node): return (
|
|
||||||
node.type in ['ATTRIBUTE_SAMPLE_TEXTURE','TEXTURE'] and node.texture)
|
|
||||||
deps = []
|
deps = []
|
||||||
|
|
||||||
for node in node_tree.nodes:
|
for node in node_tree.nodes:
|
||||||
@@ -352,8 +324,6 @@ def get_node_tree_dependencies(node_tree: bpy.types.NodeTree) -> list:
|
|||||||
deps.append(node.image)
|
deps.append(node.image)
|
||||||
elif has_node_group(node):
|
elif has_node_group(node):
|
||||||
deps.append(node.node_tree)
|
deps.append(node.node_tree)
|
||||||
elif has_texture(node):
|
|
||||||
deps.append(node.texture)
|
|
||||||
|
|
||||||
return deps
|
return deps
|
||||||
|
|
||||||
@@ -384,7 +354,10 @@ def load_materials_slots(src_materials: list, dst_materials: bpy.types.bpy_prop_
|
|||||||
if mat_uuid is not None:
|
if mat_uuid is not None:
|
||||||
mat_ref = get_datablock_from_uuid(mat_uuid, None)
|
mat_ref = get_datablock_from_uuid(mat_uuid, None)
|
||||||
else:
|
else:
|
||||||
mat_ref = bpy.data.materials[mat_name]
|
mat_ref = bpy.data.materials.get(mat_name, None)
|
||||||
|
|
||||||
|
if mat_ref is None:
|
||||||
|
raise Exception(f"Material {mat_name} doesn't exist")
|
||||||
|
|
||||||
dst_materials.append(mat_ref)
|
dst_materials.append(mat_ref)
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class BlMesh(BlDatablock):
|
|||||||
loader.load(target, data)
|
loader.load(target, data)
|
||||||
|
|
||||||
# MATERIAL SLOTS
|
# MATERIAL SLOTS
|
||||||
src_materials = data.get('materials', data.get('material_list'))
|
src_materials = data.get('materials', None)
|
||||||
if src_materials:
|
if src_materials:
|
||||||
load_materials_slots(src_materials, target.materials)
|
load_materials_slots(src_materials, target.materials)
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import mathutils
|
|||||||
from replication.exception import ContextError
|
from replication.exception import ContextError
|
||||||
|
|
||||||
from .bl_datablock import BlDatablock, get_datablock_from_uuid
|
from .bl_datablock import BlDatablock, get_datablock_from_uuid
|
||||||
from .bl_material import IGNORED_SOCKETS
|
|
||||||
from .dump_anything import (
|
from .dump_anything import (
|
||||||
Dumper,
|
Dumper,
|
||||||
Loader,
|
Loader,
|
||||||
@@ -31,28 +30,16 @@ from .dump_anything import (
|
|||||||
np_dump_collection)
|
np_dump_collection)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SKIN_DATA = [
|
SKIN_DATA = [
|
||||||
'radius',
|
'radius',
|
||||||
'use_loose',
|
'use_loose',
|
||||||
'use_root'
|
'use_root'
|
||||||
]
|
]
|
||||||
|
|
||||||
if bpy.app.version[1] >= 93:
|
def get_input_index(e):
|
||||||
SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str, float)
|
return int(re.findall('[0-9]+', e)[0])
|
||||||
else:
|
|
||||||
SUPPORTED_GEOMETRY_NODE_PARAMETERS = (int, str)
|
|
||||||
logging.warning("Geometry node Float parameter not supported in \
|
|
||||||
blender 2.92.")
|
|
||||||
|
|
||||||
def get_node_group_inputs(node_group):
|
|
||||||
inputs = []
|
|
||||||
for inpt in node_group.inputs:
|
|
||||||
if inpt.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]
|
|
||||||
|
|
||||||
def dump_modifier_geometry_node_inputs(modifier: bpy.types.Modifier) -> list:
|
def dump_modifier_geometry_node_inputs(modifier: bpy.types.Modifier) -> list:
|
||||||
""" Dump geometry node modifier input properties
|
""" Dump geometry node modifier input properties
|
||||||
@@ -60,14 +47,15 @@ def dump_modifier_geometry_node_inputs(modifier: bpy.types.Modifier) -> list:
|
|||||||
:arg modifier: geometry node modifier to dump
|
:arg modifier: geometry node modifier to dump
|
||||||
:type modifier: bpy.type.Modifier
|
:type modifier: bpy.type.Modifier
|
||||||
"""
|
"""
|
||||||
|
inputs_name = [p for p in dir(modifier) if "Input_" in p]
|
||||||
|
inputs_name.sort(key=get_input_index)
|
||||||
dumped_inputs = []
|
dumped_inputs = []
|
||||||
for inpt in get_node_group_inputs(modifier.node_group):
|
for inputs_index, input_name in enumerate(inputs_name):
|
||||||
input_value = modifier[inpt.identifier]
|
input_value = modifier[input_name]
|
||||||
|
|
||||||
dumped_input = None
|
dumped_input = None
|
||||||
if isinstance(input_value, bpy.types.ID):
|
if isinstance(input_value, bpy.types.ID):
|
||||||
dumped_input = input_value.uuid
|
dumped_input = input_value.uuid
|
||||||
elif isinstance(input_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS):
|
elif type(input_value) in [int, str, float]:
|
||||||
dumped_input = input_value
|
dumped_input = input_value
|
||||||
elif hasattr(input_value, 'to_list'):
|
elif hasattr(input_value, 'to_list'):
|
||||||
dumped_input = input_value.to_list()
|
dumped_input = input_value.to_list()
|
||||||
@@ -85,16 +73,18 @@ def load_modifier_geometry_node_inputs(dumped_modifier: dict, target_modifier: b
|
|||||||
:type target_modifier: bpy.type.Modifier
|
:type target_modifier: bpy.type.Modifier
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for input_index, inpt in enumerate(get_node_group_inputs(target_modifier.node_group)):
|
inputs_name = [p for p in dir(target_modifier) if "Input_" in p]
|
||||||
|
inputs_name.sort(key=get_input_index)
|
||||||
|
for input_index, input_name in enumerate(inputs_name):
|
||||||
dumped_value = dumped_modifier['inputs'][input_index]
|
dumped_value = dumped_modifier['inputs'][input_index]
|
||||||
input_value = target_modifier[inpt.identifier]
|
input_value = target_modifier[input_name]
|
||||||
if isinstance(input_value, SUPPORTED_GEOMETRY_NODE_PARAMETERS):
|
if type(input_value) in [int, str, float]:
|
||||||
target_modifier[inpt.identifier] = dumped_value
|
input_value = dumped_value
|
||||||
elif hasattr(input_value, 'to_list'):
|
elif hasattr(input_value, 'to_list'):
|
||||||
for index in range(len(input_value)):
|
for index in range(len(input_value)):
|
||||||
input_value[index] = dumped_value[index]
|
input_value[index] = dumped_value[index]
|
||||||
elif inpt.type in ['COLLECTION', 'OBJECT']:
|
else:
|
||||||
target_modifier[inpt.identifier] = get_datablock_from_uuid(
|
target_modifier[input_name] = get_datablock_from_uuid(
|
||||||
dumped_value, None)
|
dumped_value, None)
|
||||||
|
|
||||||
|
|
||||||
@@ -171,24 +161,19 @@ def find_textures_dependencies(modifiers: bpy.types.bpy_prop_collection) -> [bpy
|
|||||||
return textures
|
return textures
|
||||||
|
|
||||||
|
|
||||||
def find_geometry_nodes_dependencies(modifiers: bpy.types.bpy_prop_collection) -> [bpy.types.NodeTree]:
|
def find_geometry_nodes(modifiers: bpy.types.bpy_prop_collection) -> [bpy.types.NodeTree]:
|
||||||
""" Find geometry nodes dependencies from a modifier stack
|
""" Find geometry nodes group from a modifier stack
|
||||||
|
|
||||||
:arg modifiers: modifiers collection
|
:arg modifiers: modifiers collection
|
||||||
:type modifiers: bpy.types.bpy_prop_collection
|
:type modifiers: bpy.types.bpy_prop_collection
|
||||||
:return: list of bpy.types.NodeTree pointers
|
:return: list of bpy.types.NodeTree pointers
|
||||||
"""
|
"""
|
||||||
dependencies = []
|
nodes_groups = []
|
||||||
for mod in modifiers:
|
for item in modifiers:
|
||||||
if mod.type == 'NODES' and mod.node_group:
|
if item.type == 'NODES' and item.node_group:
|
||||||
dependencies.append(mod.node_group)
|
nodes_groups.append(item.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)
|
|
||||||
logging.info(dependencies)
|
|
||||||
return dependencies
|
|
||||||
|
|
||||||
|
return nodes_groups
|
||||||
|
|
||||||
def dump_vertex_groups(src_object: bpy.types.Object) -> dict:
|
def dump_vertex_groups(src_object: bpy.types.Object) -> dict:
|
||||||
""" Dump object's vertex groups
|
""" Dump object's vertex groups
|
||||||
@@ -234,7 +219,6 @@ def load_vertex_groups(dumped_vertex_groups: dict, target_object: bpy.types.Obje
|
|||||||
for index, weight in vg['vertices']:
|
for index, weight in vg['vertices']:
|
||||||
vertex_group.add([index], weight, 'REPLACE')
|
vertex_group.add([index], weight, 'REPLACE')
|
||||||
|
|
||||||
|
|
||||||
class BlObject(BlDatablock):
|
class BlObject(BlDatablock):
|
||||||
bl_id = "objects"
|
bl_id = "objects"
|
||||||
bl_class = bpy.types.Object
|
bl_class = bpy.types.Object
|
||||||
@@ -312,20 +296,9 @@ class BlObject(BlDatablock):
|
|||||||
# Load transformation data
|
# Load transformation data
|
||||||
loader.load(target, data)
|
loader.load(target, data)
|
||||||
|
|
||||||
# Object display fields
|
|
||||||
if 'display' in data:
|
if 'display' in data:
|
||||||
loader.load(target.display, data['display'])
|
loader.load(target.display, data['display'])
|
||||||
|
|
||||||
# Parenting
|
|
||||||
parent_id = data.get('parent_id')
|
|
||||||
if parent_id:
|
|
||||||
parent = bpy.data.objects[parent_id]
|
|
||||||
# Avoid reloading
|
|
||||||
if target.parent != parent and parent is not None:
|
|
||||||
target.parent = parent
|
|
||||||
elif target.parent:
|
|
||||||
target.parent = None
|
|
||||||
|
|
||||||
# Pose
|
# Pose
|
||||||
if 'pose' in data:
|
if 'pose' in data:
|
||||||
if not target.pose:
|
if not target.pose:
|
||||||
@@ -370,23 +343,14 @@ class BlObject(BlDatablock):
|
|||||||
SKIN_DATA)
|
SKIN_DATA)
|
||||||
|
|
||||||
if hasattr(target, 'cycles_visibility') \
|
if hasattr(target, 'cycles_visibility') \
|
||||||
and 'cycles_visibility' in data:
|
and 'cycles_visibility' in data:
|
||||||
loader.load(target.cycles_visibility, data['cycles_visibility'])
|
loader.load(target.cycles_visibility, data['cycles_visibility'])
|
||||||
|
|
||||||
# TODO: handle geometry nodes input from dump_anything
|
# TODO: handle geometry nodes input from dump_anything
|
||||||
if hasattr(target, 'modifiers'):
|
if hasattr(target, 'modifiers'):
|
||||||
nodes_modifiers = [
|
nodes_modifiers = [mod for mod in target.modifiers if mod.type == 'NODES']
|
||||||
mod for mod in target.modifiers if mod.type == 'NODES']
|
|
||||||
for modifier in nodes_modifiers:
|
for modifier in nodes_modifiers:
|
||||||
load_modifier_geometry_node_inputs(
|
load_modifier_geometry_node_inputs(data['modifiers'][modifier.name], modifier)
|
||||||
data['modifiers'][modifier.name], modifier)
|
|
||||||
|
|
||||||
transform = data.get('transforms', None)
|
|
||||||
if transform:
|
|
||||||
target.matrix_parent_inverse = mathutils.Matrix(
|
|
||||||
transform['matrix_parent_inverse'])
|
|
||||||
target.matrix_basis = mathutils.Matrix(transform['matrix_basis'])
|
|
||||||
target.matrix_local = mathutils.Matrix(transform['matrix_local'])
|
|
||||||
|
|
||||||
def _dump_implementation(self, data, instance=None):
|
def _dump_implementation(self, data, instance=None):
|
||||||
assert(instance)
|
assert(instance)
|
||||||
@@ -402,6 +366,7 @@ class BlObject(BlDatablock):
|
|||||||
dumper.include_filter = [
|
dumper.include_filter = [
|
||||||
"name",
|
"name",
|
||||||
"rotation_mode",
|
"rotation_mode",
|
||||||
|
"parent",
|
||||||
"data",
|
"data",
|
||||||
"library",
|
"library",
|
||||||
"empty_display_type",
|
"empty_display_type",
|
||||||
@@ -416,6 +381,8 @@ class BlObject(BlDatablock):
|
|||||||
"color",
|
"color",
|
||||||
"instance_collection",
|
"instance_collection",
|
||||||
"instance_type",
|
"instance_type",
|
||||||
|
"location",
|
||||||
|
"scale",
|
||||||
'lock_location',
|
'lock_location',
|
||||||
'lock_rotation',
|
'lock_rotation',
|
||||||
'lock_scale',
|
'lock_scale',
|
||||||
@@ -429,16 +396,12 @@ class BlObject(BlDatablock):
|
|||||||
'show_all_edges',
|
'show_all_edges',
|
||||||
'show_texture_space',
|
'show_texture_space',
|
||||||
'show_in_front',
|
'show_in_front',
|
||||||
'type'
|
'type',
|
||||||
|
'rotation_quaternion' if instance.rotation_mode == 'QUATERNION' else 'rotation_euler',
|
||||||
]
|
]
|
||||||
|
|
||||||
data = dumper.dump(instance)
|
data = dumper.dump(instance)
|
||||||
|
|
||||||
dumper.include_filter = [
|
|
||||||
'matrix_parent_inverse',
|
|
||||||
'matrix_local',
|
|
||||||
'matrix_basis']
|
|
||||||
data['transforms'] = dumper.dump(instance)
|
|
||||||
dumper.include_filter = [
|
dumper.include_filter = [
|
||||||
'show_shadows',
|
'show_shadows',
|
||||||
]
|
]
|
||||||
@@ -448,10 +411,6 @@ class BlObject(BlDatablock):
|
|||||||
if self.is_library:
|
if self.is_library:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
# PARENTING
|
|
||||||
if instance.parent:
|
|
||||||
data['parent_id'] = instance.parent.name
|
|
||||||
|
|
||||||
# MODIFIERS
|
# MODIFIERS
|
||||||
if hasattr(instance, 'modifiers'):
|
if hasattr(instance, 'modifiers'):
|
||||||
data["modifiers"] = {}
|
data["modifiers"] = {}
|
||||||
@@ -463,8 +422,7 @@ class BlObject(BlDatablock):
|
|||||||
data["modifiers"][modifier.name] = dumper.dump(modifier)
|
data["modifiers"][modifier.name] = dumper.dump(modifier)
|
||||||
# hack to dump geometry nodes inputs
|
# hack to dump geometry nodes inputs
|
||||||
if modifier.type == 'NODES':
|
if modifier.type == 'NODES':
|
||||||
dumped_inputs = dump_modifier_geometry_node_inputs(
|
dumped_inputs = dump_modifier_geometry_node_inputs(modifier)
|
||||||
modifier)
|
|
||||||
data["modifiers"][modifier.name]['inputs'] = dumped_inputs
|
data["modifiers"][modifier.name]['inputs'] = dumped_inputs
|
||||||
gp_modifiers = getattr(instance, 'grease_pencil_modifiers', None)
|
gp_modifiers = getattr(instance, 'grease_pencil_modifiers', None)
|
||||||
|
|
||||||
@@ -531,6 +489,7 @@ class BlObject(BlDatablock):
|
|||||||
bone_groups[group.name] = dumper.dump(group)
|
bone_groups[group.name] = dumper.dump(group)
|
||||||
data['pose']['bone_groups'] = bone_groups
|
data['pose']['bone_groups'] = bone_groups
|
||||||
|
|
||||||
|
|
||||||
# VERTEx GROUP
|
# VERTEx GROUP
|
||||||
if len(instance.vertex_groups) > 0:
|
if len(instance.vertex_groups) > 0:
|
||||||
data['vertex_groups'] = dump_vertex_groups(instance)
|
data['vertex_groups'] = dump_vertex_groups(instance)
|
||||||
@@ -567,8 +526,7 @@ class BlObject(BlDatablock):
|
|||||||
if hasattr(object_data, 'skin_vertices') and object_data.skin_vertices:
|
if hasattr(object_data, 'skin_vertices') and object_data.skin_vertices:
|
||||||
skin_vertices = list()
|
skin_vertices = list()
|
||||||
for skin_data in object_data.skin_vertices:
|
for skin_data in object_data.skin_vertices:
|
||||||
skin_vertices.append(
|
skin_vertices.append(np_dump_collection(skin_data.data, SKIN_DATA))
|
||||||
np_dump_collection(skin_data.data, SKIN_DATA))
|
|
||||||
data['skin_vertices'] = skin_vertices
|
data['skin_vertices'] = skin_vertices
|
||||||
|
|
||||||
# CYCLE SETTINGS
|
# CYCLE SETTINGS
|
||||||
@@ -591,19 +549,18 @@ class BlObject(BlDatablock):
|
|||||||
# Avoid Empty case
|
# Avoid Empty case
|
||||||
if self.instance.data:
|
if self.instance.data:
|
||||||
deps.append(self.instance.data)
|
deps.append(self.instance.data)
|
||||||
|
if self.instance.parent :
|
||||||
|
deps.append(self.instance.parent)
|
||||||
|
|
||||||
if self.is_library:
|
if self.is_library:
|
||||||
deps.append(self.instance.library)
|
deps.append(self.instance.library)
|
||||||
|
|
||||||
if self.instance.parent:
|
|
||||||
deps.append(self.instance.parent)
|
|
||||||
|
|
||||||
if self.instance.instance_type == 'COLLECTION':
|
if self.instance.instance_type == 'COLLECTION':
|
||||||
# TODO: uuid based
|
# TODO: uuid based
|
||||||
deps.append(self.instance.instance_collection)
|
deps.append(self.instance.instance_collection)
|
||||||
|
|
||||||
if self.instance.modifiers:
|
if self.instance.modifiers:
|
||||||
deps.extend(find_textures_dependencies(self.instance.modifiers))
|
deps.extend(find_textures_dependencies(self.instance.modifiers))
|
||||||
deps.extend(find_geometry_nodes_dependencies(self.instance.modifiers))
|
deps.extend(find_geometry_nodes(self.instance.modifiers))
|
||||||
|
|
||||||
return deps
|
return deps
|
||||||
|
|||||||
@@ -56,10 +56,6 @@ def np_load_collection(dikt: dict, collection: bpy.types.CollectionProperty, att
|
|||||||
for attr in attributes:
|
for attr in attributes:
|
||||||
attr_type = collection[0].bl_rna.properties.get(attr).type
|
attr_type = collection[0].bl_rna.properties.get(attr).type
|
||||||
|
|
||||||
if attr not in dikt:
|
|
||||||
logging.warning(f"No data for {attr}, skipping.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if attr_type in PRIMITIVE_TYPES:
|
if attr_type in PRIMITIVE_TYPES:
|
||||||
np_load_collection_primitives(collection, attr, dikt[attr])
|
np_load_collection_primitives(collection, attr, dikt[attr])
|
||||||
elif attr_type == 'ENUM':
|
elif attr_type == 'ENUM':
|
||||||
|
|||||||
Submodule multi_user/libs/replication deleted from 9a02e16d70
+79
-225
@@ -39,7 +39,6 @@ except ImportError:
|
|||||||
import pickle
|
import pickle
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import bmesh
|
|
||||||
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
|
||||||
@@ -57,7 +56,6 @@ background_execution_queue = Queue()
|
|||||||
deleyables = []
|
deleyables = []
|
||||||
stop_modal_executor = False
|
stop_modal_executor = False
|
||||||
|
|
||||||
|
|
||||||
def session_callback(name):
|
def session_callback(name):
|
||||||
""" Session callback wrapper
|
""" Session callback wrapper
|
||||||
|
|
||||||
@@ -139,8 +137,7 @@ def on_connection_end(reason="none"):
|
|||||||
if isinstance(handler, logging.FileHandler):
|
if isinstance(handler, logging.FileHandler):
|
||||||
logger.removeHandler(handler)
|
logger.removeHandler(handler)
|
||||||
if reason != "user":
|
if reason != "user":
|
||||||
bpy.ops.session.notify(
|
bpy.ops.session.notify('INVOKE_DEFAULT', message=f"Disconnected from session. Reason: {reason}. ")
|
||||||
'INVOKE_DEFAULT', message=f"Disconnected from session. Reason: {reason}. ")
|
|
||||||
|
|
||||||
|
|
||||||
# OPERATORS
|
# OPERATORS
|
||||||
@@ -213,6 +210,8 @@ class SessionStartOperator(bpy.types.Operator):
|
|||||||
type_module_class,
|
type_module_class,
|
||||||
check_common=type_module_class.bl_check_common)
|
check_common=type_module_class.bl_check_common)
|
||||||
|
|
||||||
|
deleyables.append(timers.ApplyTimer(timeout=settings.depsgraph_update_rate))
|
||||||
|
|
||||||
if bpy.app.version[1] >= 91:
|
if bpy.app.version[1] >= 91:
|
||||||
python_binary_path = sys.executable
|
python_binary_path = sys.executable
|
||||||
else:
|
else:
|
||||||
@@ -273,8 +272,6 @@ class SessionStartOperator(bpy.types.Operator):
|
|||||||
# Background client updates service
|
# Background client updates service
|
||||||
deleyables.append(timers.ClientUpdate())
|
deleyables.append(timers.ClientUpdate())
|
||||||
deleyables.append(timers.DynamicRightSelectTimer())
|
deleyables.append(timers.DynamicRightSelectTimer())
|
||||||
deleyables.append(timers.ApplyTimer(
|
|
||||||
timeout=settings.depsgraph_update_rate))
|
|
||||||
# deleyables.append(timers.PushTimer(
|
# deleyables.append(timers.PushTimer(
|
||||||
# queue=stagging,
|
# queue=stagging,
|
||||||
# timeout=settings.depsgraph_update_rate
|
# timeout=settings.depsgraph_update_rate
|
||||||
@@ -292,6 +289,8 @@ class SessionStartOperator(bpy.types.Operator):
|
|||||||
deleyables.append(session_update)
|
deleyables.append(session_update)
|
||||||
deleyables.append(session_user_sync)
|
deleyables.append(session_user_sync)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
self.report(
|
self.report(
|
||||||
{'INFO'},
|
{'INFO'},
|
||||||
f"connecting to tcp://{settings.ip}:{settings.port}")
|
f"connecting to tcp://{settings.ip}:{settings.port}")
|
||||||
@@ -637,7 +636,6 @@ class SessionCommit(bpy.types.Operator):
|
|||||||
self.report({'ERROR'}, repr(e))
|
self.report({'ERROR'}, repr(e))
|
||||||
return {"CANCELED"}
|
return {"CANCELED"}
|
||||||
|
|
||||||
|
|
||||||
class ApplyArmatureOperator(bpy.types.Operator):
|
class ApplyArmatureOperator(bpy.types.Operator):
|
||||||
"""Operator which runs its self from a timer"""
|
"""Operator which runs its self from a timer"""
|
||||||
bl_idname = "session.apply_armature_operator"
|
bl_idname = "session.apply_armature_operator"
|
||||||
@@ -709,7 +707,6 @@ class SessionClearCache(bpy.types.Operator):
|
|||||||
row = self.layout
|
row = self.layout
|
||||||
row.label(text=f" Do you really want to remove local cache ? ")
|
row.label(text=f" Do you really want to remove local cache ? ")
|
||||||
|
|
||||||
|
|
||||||
class SessionPurgeOperator(bpy.types.Operator):
|
class SessionPurgeOperator(bpy.types.Operator):
|
||||||
"Remove node with lost references"
|
"Remove node with lost references"
|
||||||
bl_idname = "session.purge"
|
bl_idname = "session.purge"
|
||||||
@@ -754,6 +751,7 @@ 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)
|
||||||
|
|
||||||
@@ -799,7 +797,6 @@ class SessionSaveBackupOperator(bpy.types.Operator, ExportHelper):
|
|||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return session.state['STATE'] == STATE_ACTIVE
|
return session.state['STATE'] == STATE_ACTIVE
|
||||||
|
|
||||||
|
|
||||||
class SessionStopAutoSaveOperator(bpy.types.Operator):
|
class SessionStopAutoSaveOperator(bpy.types.Operator):
|
||||||
bl_idname = "session.cancel_autosave"
|
bl_idname = "session.cancel_autosave"
|
||||||
bl_label = "Cancel auto-save"
|
bl_label = "Cancel auto-save"
|
||||||
@@ -831,222 +828,79 @@ class SessionLoadSaveOperator(bpy.types.Operator, ImportHelper):
|
|||||||
maxlen=255, # Max internal buffer length, longer would be clamped.
|
maxlen=255, # Max internal buffer length, longer would be clamped.
|
||||||
)
|
)
|
||||||
|
|
||||||
load_to_collection: bpy.props.BoolProperty(
|
|
||||||
name="Load to collection",
|
|
||||||
description="Load the snapshot into a collection",
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
draw_users: bpy.props.BoolProperty(
|
|
||||||
name="Draw users",
|
|
||||||
description="Draw a mesh representing each user position and selected object",
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
animate: bpy.props.BoolProperty(
|
|
||||||
name="Animate history",
|
|
||||||
description="Animate history versions",
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
clear_datablocks: bpy.props.BoolProperty(
|
|
||||||
name="Removes existing data",
|
|
||||||
description="Remove all exisitng datablocks",
|
|
||||||
default=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
files: bpy.props.CollectionProperty(
|
|
||||||
type=bpy.types.OperatorFileListElement,
|
|
||||||
options={'HIDDEN', 'SKIP_SAVE'},
|
|
||||||
)
|
|
||||||
|
|
||||||
def draw_users_as_curves(self, user_list):
|
|
||||||
users_collection = bpy.data.collections.new("users")
|
|
||||||
for username, user_data in user_list.items():
|
|
||||||
metadata = user_data.get('metadata', None)
|
|
||||||
if metadata:
|
|
||||||
logging.info(f"Loading user {username}")
|
|
||||||
|
|
||||||
# Curve creation
|
|
||||||
location = metadata.get('view_corners')
|
|
||||||
positions = [coord for coord in location]
|
|
||||||
|
|
||||||
curve = bpy.data.curves.new(username, 'CURVE')
|
|
||||||
obj = bpy.data.objects.new(username, curve)
|
|
||||||
|
|
||||||
for p in positions:
|
|
||||||
p.append(0.0)
|
|
||||||
|
|
||||||
sight = curve.splines.new('POLY')
|
|
||||||
sight.points.add(1)
|
|
||||||
sight.points[0].co = positions[4]
|
|
||||||
sight.points[1].co = positions[5]
|
|
||||||
|
|
||||||
cadre = curve.splines.new('POLY')
|
|
||||||
cadre.points.add(4)
|
|
||||||
cadre.points[0].co = positions[0]
|
|
||||||
cadre.points[1].co = positions[2]
|
|
||||||
cadre.points[2].co = positions[1]
|
|
||||||
cadre.points[3].co = positions[3]
|
|
||||||
cadre.points[4].co = positions[0]
|
|
||||||
|
|
||||||
frust = curve.splines.new('POLY')
|
|
||||||
frust.points.add(2)
|
|
||||||
frust.points[0].co = positions[0]
|
|
||||||
frust.points[1].co = positions[6]
|
|
||||||
frust.points[2].co = positions[1]
|
|
||||||
|
|
||||||
frust2 = curve.splines.new('POLY')
|
|
||||||
frust2.points.add(2)
|
|
||||||
frust2.points[0].co = positions[2]
|
|
||||||
frust2.points[1].co = positions[6]
|
|
||||||
frust2.points[2].co = positions[3]
|
|
||||||
|
|
||||||
curve.bevel_depth = 0.02
|
|
||||||
|
|
||||||
# Material creation
|
|
||||||
color = metadata.get('color')
|
|
||||||
material = bpy.data.materials.new(username)
|
|
||||||
material.use_nodes = True
|
|
||||||
material.node_tree.nodes[0].inputs['Emission'].default_value = color
|
|
||||||
|
|
||||||
curve.materials.append(material)
|
|
||||||
|
|
||||||
users_collection.objects.link(obj)
|
|
||||||
return users_collection
|
|
||||||
|
|
||||||
def draw_users_meshes(self, user_list):
|
|
||||||
for username, user_data in user_list.items():
|
|
||||||
metadata = user_data.get('metadata', None)
|
|
||||||
if metadata:
|
|
||||||
logging.info(f"Loading user {username}")
|
|
||||||
location = metadata.get('view_corners')
|
|
||||||
color = metadata.get('color')
|
|
||||||
|
|
||||||
positions = [tuple(coord) for coord in location]
|
|
||||||
edges = ((1, 3), (2, 1), (3, 0),
|
|
||||||
(2, 0), (4, 5), (1, 6),
|
|
||||||
(2, 6), (3, 6), (0, 6))
|
|
||||||
|
|
||||||
mesh = bpy.data.meshes.new("toto")
|
|
||||||
obj = bpy.data.objects.new("toto", mesh)
|
|
||||||
bm = bmesh.new()
|
|
||||||
|
|
||||||
for p in positions:
|
|
||||||
bm.verts.new(p)
|
|
||||||
bm.verts.ensure_lookup_table()
|
|
||||||
|
|
||||||
for v1, v2 in edges:
|
|
||||||
bm.edges.new((bm.verts[v1], bm.verts[v2]))
|
|
||||||
bm.to_mesh(mesh)
|
|
||||||
|
|
||||||
bpy.context.scene.collection.objects.link(obj)
|
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
from replication.graph import ReplicationGraph
|
from replication.graph import ReplicationGraph
|
||||||
|
|
||||||
# Initialisation
|
# TODO: add filechecks
|
||||||
# init the factory with supported types
|
|
||||||
bpy_factory=ReplicatedDataFactory()
|
try:
|
||||||
for type in bl_types.types_to_register():
|
f = gzip.open(self.filepath, "rb")
|
||||||
type_module=getattr(bl_types, type)
|
db = pickle.load(f)
|
||||||
name=[e.capitalize() for e in type.split('_')[1:]]
|
except OSError as e:
|
||||||
type_impl_name='Bl'+''.join(name)
|
f = open(self.filepath, "rb")
|
||||||
type_module_class=getattr(type_module, type_impl_name)
|
db = pickle.load(f)
|
||||||
|
|
||||||
|
if db:
|
||||||
|
logging.info(f"Reading {self.filepath}")
|
||||||
|
nodes = db.get("nodes")
|
||||||
|
|
||||||
|
logging.info(f"{len(nodes)} Nodes to load")
|
||||||
|
|
||||||
|
|
||||||
bpy_factory.register_type(
|
|
||||||
type_module_class.bl_class,
|
# init the factory with supported types
|
||||||
type_module_class)
|
bpy_factory = ReplicatedDataFactory()
|
||||||
# Optionnaly clear the scene
|
for type in bl_types.types_to_register():
|
||||||
if self.clear_datablocks:
|
type_module = getattr(bl_types, type)
|
||||||
|
name = [e.capitalize() for e in type.split('_')[1:]]
|
||||||
|
type_impl_name = 'Bl'+''.join(name)
|
||||||
|
type_module_class = getattr(type_module, type_impl_name)
|
||||||
|
|
||||||
|
|
||||||
|
bpy_factory.register_type(
|
||||||
|
type_module_class.bl_class,
|
||||||
|
type_module_class)
|
||||||
|
|
||||||
|
graph = ReplicationGraph()
|
||||||
|
|
||||||
|
for node, node_data in nodes:
|
||||||
|
node_type = node_data.get('str_type')
|
||||||
|
|
||||||
|
impl = bpy_factory.get_implementation_from_net(node_type)
|
||||||
|
|
||||||
|
if impl:
|
||||||
|
logging.info(f"Loading {node}")
|
||||||
|
instance = impl(owner=node_data['owner'],
|
||||||
|
uuid=node,
|
||||||
|
dependencies=node_data['dependencies'],
|
||||||
|
data=node_data['data'])
|
||||||
|
instance.store(graph)
|
||||||
|
instance.state = FETCHED
|
||||||
|
|
||||||
|
logging.info("Graph succefully loaded")
|
||||||
|
|
||||||
utils.clean_scene()
|
utils.clean_scene()
|
||||||
|
|
||||||
dir_path=Path(self.filepath).parent
|
# Step 1: Construct nodes
|
||||||
|
for node in graph.list_ordered():
|
||||||
|
graph[node].resolve()
|
||||||
|
|
||||||
for db in self.files:
|
# Step 2: Load nodes
|
||||||
filepath=os.path.join(dir_path, db.name)
|
for node in graph.list_ordered():
|
||||||
|
graph[node].apply()
|
||||||
try:
|
|
||||||
f=gzip.open(filepath, "rb")
|
|
||||||
db=pickle.load(f)
|
|
||||||
except OSError as e:
|
|
||||||
f=open(filepath, "rb")
|
|
||||||
db=pickle.load(f)
|
|
||||||
|
|
||||||
if db:
|
|
||||||
created=os.path.getctime(filepath)
|
|
||||||
logging.info(f"Reading {filepath}")
|
|
||||||
nodes=db.get("nodes")
|
|
||||||
users=db.get("users")
|
|
||||||
users_collection = self.draw_users_as_curves(users)
|
|
||||||
logging.info(f"{len(nodes)} Nodes to load")
|
|
||||||
|
|
||||||
graph=ReplicationGraph()
|
|
||||||
|
|
||||||
for node, node_data in nodes:
|
|
||||||
node_type=node_data.get('str_type')
|
|
||||||
|
|
||||||
impl=bpy_factory.get_implementation_from_net(node_type)
|
|
||||||
|
|
||||||
if impl:
|
|
||||||
logging.info(f"Loading {node}")
|
|
||||||
instance=impl(owner=node_data['owner'],
|
|
||||||
uuid=node,
|
|
||||||
dependencies=node_data['dependencies'],
|
|
||||||
data=node_data['data'])
|
|
||||||
instance.store(graph)
|
|
||||||
instance.state=FETCHED
|
|
||||||
|
|
||||||
logging.info("Graph succefully loaded")
|
|
||||||
|
|
||||||
# Find scene
|
|
||||||
scenes=[n for n in graph.values() if isinstance(
|
|
||||||
n, bl_types.bl_scene.BlScene)]
|
|
||||||
scene=scenes[0]
|
|
||||||
|
|
||||||
# collection_data = {
|
|
||||||
# 'instance_offset': [0.0, 0.0, 0.0],
|
|
||||||
# 'name': str(created),
|
|
||||||
# 'objects': scene.data['collection']['objects'],
|
|
||||||
# 'children': scene.data['collection']['children']}
|
|
||||||
# collection_node = bl_types.bl_collection.BlCollection()
|
|
||||||
# collection_node.dependencies = scene.dependencies
|
|
||||||
# collection_node.data = collection_data
|
|
||||||
# graph[collection_node.uuid] = collection_node
|
|
||||||
# del graph[scene.uuid]
|
|
||||||
scene.data['name']=str(created)
|
|
||||||
|
|
||||||
# Step 1: Construct nodes
|
|
||||||
for node in graph.list_ordered():
|
|
||||||
node_inst=graph[node]
|
|
||||||
try:
|
|
||||||
node_inst.instance=node_inst._construct(node_inst.data)
|
|
||||||
node_inst.instance.uuid=node_inst.uuid
|
|
||||||
except Exception as e:
|
|
||||||
continue
|
|
||||||
# Step 2: Load nodes
|
|
||||||
for node in graph.list_ordered():
|
|
||||||
graph[node].state=FETCHED
|
|
||||||
graph[node].apply()
|
|
||||||
|
|
||||||
scene.instance.collection.children.link(users_collection)
|
|
||||||
|
|
||||||
# bpy.context.scene.collection.children.link(collection_node.instance)
|
|
||||||
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
@ classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def menu_func_import(self, context):
|
def menu_func_import(self, context):
|
||||||
self.layout.operator(SessionLoadSaveOperator.bl_idname,
|
self.layout.operator(SessionLoadSaveOperator.bl_idname, text='Multi-user session snapshot (.db)')
|
||||||
text='Multi-user session snapshot (.db)')
|
|
||||||
|
|
||||||
|
|
||||||
classes=(
|
classes = (
|
||||||
SessionStartOperator,
|
SessionStartOperator,
|
||||||
SessionStopOperator,
|
SessionStopOperator,
|
||||||
SessionPropertyRemoveOperator,
|
SessionPropertyRemoveOperator,
|
||||||
@@ -1067,22 +921,22 @@ classes=(
|
|||||||
)
|
)
|
||||||
|
|
||||||
def update_external_dependencies():
|
def update_external_dependencies():
|
||||||
nodes_ids=session.list(filter=bl_types.bl_file.BlFile)
|
nodes_ids = session.list(filter=bl_types.bl_file.BlFile)
|
||||||
for node_id in nodes_ids:
|
for node_id in nodes_ids:
|
||||||
node=session.get(node_id)
|
node = session.get(node_id)
|
||||||
if node and node.owner in [session.id, RP_COMMON] \
|
if node and node.owner in [session.id, RP_COMMON] \
|
||||||
and node.has_changed():
|
and node.has_changed():
|
||||||
session.commit(node_id)
|
session.commit(node_id)
|
||||||
session.push(node_id, check_data=False)
|
session.push(node_id, check_data=False)
|
||||||
|
|
||||||
def sanitize_deps_graph(remove_nodes: bool=False):
|
def sanitize_deps_graph(remove_nodes: bool = False):
|
||||||
""" Cleanup the replication graph
|
""" Cleanup the replication graph
|
||||||
"""
|
"""
|
||||||
if session and session.state['STATE'] == STATE_ACTIVE:
|
if session and session.state['STATE'] == STATE_ACTIVE:
|
||||||
start=utils.current_milli_time()
|
start = utils.current_milli_time()
|
||||||
rm_cpt=0
|
rm_cpt = 0
|
||||||
for node_key in session.list():
|
for node_key in session.list():
|
||||||
node=session.get(node_key)
|
node = session.get(node_key)
|
||||||
if node is None \
|
if node is None \
|
||||||
or (node.state == UP and not node.resolve(construct=False)):
|
or (node.state == UP and not node.resolve(construct=False)):
|
||||||
if remove_nodes:
|
if remove_nodes:
|
||||||
@@ -1095,7 +949,7 @@ def sanitize_deps_graph(remove_nodes: bool=False):
|
|||||||
logging.info(f"Sanitize took { utils.current_milli_time()-start} ms")
|
logging.info(f"Sanitize took { utils.current_milli_time()-start} ms")
|
||||||
|
|
||||||
|
|
||||||
@ persistent
|
@persistent
|
||||||
def resolve_deps_graph(dummy):
|
def resolve_deps_graph(dummy):
|
||||||
"""Resolve deps graph
|
"""Resolve deps graph
|
||||||
|
|
||||||
@@ -1106,13 +960,13 @@ def resolve_deps_graph(dummy):
|
|||||||
if session and session.state['STATE'] == STATE_ACTIVE:
|
if session and session.state['STATE'] == STATE_ACTIVE:
|
||||||
sanitize_deps_graph(remove_nodes=True)
|
sanitize_deps_graph(remove_nodes=True)
|
||||||
|
|
||||||
@ persistent
|
@persistent
|
||||||
def load_pre_handler(dummy):
|
def load_pre_handler(dummy):
|
||||||
if session and session.state['STATE'] in [STATE_ACTIVE, STATE_SYNCING]:
|
if session and session.state['STATE'] in [STATE_ACTIVE, STATE_SYNCING]:
|
||||||
bpy.ops.session.stop()
|
bpy.ops.session.stop()
|
||||||
|
|
||||||
|
|
||||||
@ persistent
|
@persistent
|
||||||
def update_client_frame(scene):
|
def update_client_frame(scene):
|
||||||
if session and session.state['STATE'] == STATE_ACTIVE:
|
if session and session.state['STATE'] == STATE_ACTIVE:
|
||||||
session.update_user_metadata({
|
session.update_user_metadata({
|
||||||
@@ -1120,13 +974,13 @@ def update_client_frame(scene):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ persistent
|
@persistent
|
||||||
def depsgraph_evaluation(scene):
|
def depsgraph_evaluation(scene):
|
||||||
if session and session.state['STATE'] == STATE_ACTIVE:
|
if session and session.state['STATE'] == STATE_ACTIVE:
|
||||||
context=bpy.context
|
context = bpy.context
|
||||||
blender_depsgraph=bpy.context.view_layer.depsgraph
|
blender_depsgraph = bpy.context.view_layer.depsgraph
|
||||||
dependency_updates=[u for u in blender_depsgraph.updates]
|
dependency_updates = [u for u in blender_depsgraph.updates]
|
||||||
settings=utils.get_preferences()
|
settings = utils.get_preferences()
|
||||||
|
|
||||||
update_external_dependencies()
|
update_external_dependencies()
|
||||||
|
|
||||||
@@ -1135,13 +989,13 @@ def depsgraph_evaluation(scene):
|
|||||||
# Is the object tracked ?
|
# Is the object tracked ?
|
||||||
if update.id.uuid:
|
if update.id.uuid:
|
||||||
# Retrieve local version
|
# Retrieve local version
|
||||||
node=session.get(uuid=update.id.uuid)
|
node = session.get(uuid=update.id.uuid)
|
||||||
|
|
||||||
# Check our right on this update:
|
# Check our right on this update:
|
||||||
# - if its ours or ( under common and diff), launch the
|
# - if its ours or ( under common and diff), launch the
|
||||||
# update process
|
# update process
|
||||||
# - if its to someone else, ignore the update
|
# - if its to someone else, ignore the update
|
||||||
if node and (node.owner == session.id or node.bl_check_common):
|
if node and node.owner in [session.id, RP_COMMON]:
|
||||||
if node.state == UP:
|
if node.state == UP:
|
||||||
try:
|
try:
|
||||||
if node.has_changed():
|
if node.has_changed():
|
||||||
@@ -1159,11 +1013,11 @@ def depsgraph_evaluation(scene):
|
|||||||
continue
|
continue
|
||||||
# A new scene is created
|
# A new scene is created
|
||||||
elif isinstance(update.id, bpy.types.Scene):
|
elif isinstance(update.id, bpy.types.Scene):
|
||||||
ref=session.get(reference=update.id)
|
ref = session.get(reference=update.id)
|
||||||
if ref:
|
if ref:
|
||||||
ref.resolve()
|
ref.resolve()
|
||||||
else:
|
else:
|
||||||
scn_uuid=session.add(update.id)
|
scn_uuid = session.add(update.id)
|
||||||
session.commit(scn_uuid)
|
session.commit(scn_uuid)
|
||||||
session.push(scn_uuid, check_data=False)
|
session.push(scn_uuid, check_data=False)
|
||||||
def register():
|
def register():
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
|||||||
connection_timeout: bpy.props.IntProperty(
|
connection_timeout: bpy.props.IntProperty(
|
||||||
name='connection timeout',
|
name='connection timeout',
|
||||||
description='connection timeout before disconnection',
|
description='connection timeout before disconnection',
|
||||||
default=5000
|
default=1000
|
||||||
)
|
)
|
||||||
# Replication update settings
|
# Replication update settings
|
||||||
depsgraph_update_rate: bpy.props.FloatProperty(
|
depsgraph_update_rate: bpy.props.FloatProperty(
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
|
||||||
import bpy
|
import bpy
|
||||||
from replication.constants import (FETCHED, RP_COMMON, STATE_ACTIVE,
|
from replication.constants import (FETCHED, RP_COMMON, STATE_ACTIVE,
|
||||||
STATE_INITIAL, STATE_LOBBY, STATE_QUITTING,
|
STATE_INITIAL, STATE_LOBBY, STATE_QUITTING,
|
||||||
@@ -112,8 +112,7 @@ class ApplyTimer(Timer):
|
|||||||
try:
|
try:
|
||||||
session.apply(node)
|
session.apply(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}: {e}")
|
||||||
traceback.print_exc()
|
|
||||||
else:
|
else:
|
||||||
if node_ref.bl_reload_parent:
|
if node_ref.bl_reload_parent:
|
||||||
for parent in session._graph.find_parents(node):
|
for parent in session._graph.find_parents(node):
|
||||||
|
|||||||
Reference in New Issue
Block a user