Compare commits

..

6 Commits

14 changed files with 277 additions and 497 deletions
-1
View File
@@ -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
+5 -1
View File
@@ -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
+4 -1
View File
@@ -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
+2 -7
View File
@@ -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
View File
@@ -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
+32 -34
View File
@@ -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
View File
@@ -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)
+1 -1
View File
@@ -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)
+37 -80
View File
@@ -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
-4
View File
@@ -55,10 +55,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])
Submodule multi_user/libs/replication deleted from 9a02e16d70
+88 -234
View File
@@ -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,12 +56,11 @@ 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
This allow to encapsulate session callbacks to background_execution_queue. This allow to encapsulate session callbacks to background_execution_queue.
By doing this way callback are executed from the main thread. By doing this way callback are executed from the main thread.
""" """
def func_wrapper(func): def func_wrapper(func):
@session.register(name) @session.register(name)
@@ -74,7 +72,7 @@ def session_callback(name):
@session_callback('on_connection') @session_callback('on_connection')
def initialize_session(): def initialize_session():
"""Session connection init hander """Session connection init hander
""" """
logging.info("Intializing the scene") logging.info("Intializing the scene")
settings = utils.get_preferences() settings = utils.get_preferences()
@@ -88,7 +86,7 @@ def initialize_session():
logging.error(f"Can't construct node {node}") logging.error(f"Can't construct node {node}")
elif node_ref.state == FETCHED: elif node_ref.state == FETCHED:
node_ref.resolve() node_ref.resolve()
# Step 2: Load nodes # Step 2: Load nodes
logging.info("Loading nodes") logging.info("Loading nodes")
for node in session._graph.list_ordered(): for node in session._graph.list_ordered():
@@ -115,7 +113,7 @@ def initialize_session():
@session_callback('on_exit') @session_callback('on_exit')
def on_connection_end(reason="none"): def on_connection_end(reason="none"):
"""Session connection finished handler """Session connection finished handler
""" """
global deleyables, stop_modal_executor global deleyables, stop_modal_executor
settings = utils.get_preferences() settings = utils.get_preferences()
@@ -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}")
@@ -611,7 +610,7 @@ class SessionApply(bpy.types.Operator):
session.apply(parent, force=True) session.apply(parent, force=True)
except Exception as e: except Exception as e:
self.report({'ERROR'}, repr(e)) self.report({'ERROR'}, repr(e))
return {"CANCELED"} return {"CANCELED"}
return {"FINISHED"} return {"FINISHED"}
@@ -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")
# init the factory with supported types
bpy_factory = ReplicatedDataFactory()
for type in bl_types.types_to_register():
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( bpy_factory.register_type(
type_module_class.bl_class, type_module_class.bl_class,
type_module_class) type_module_class)
# Optionnaly clear the scene
if self.clear_datablocks: 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():
@@ -1152,24 +1006,24 @@ def depsgraph_evaluation(scene):
if not node.is_valid(): if not node.is_valid():
session.remove(node.uuid) session.remove(node.uuid)
except ContextError as e: except ContextError as e:
logging.debug(e) logging.debug(e)
except Exception as e: except Exception as e:
logging.error(e) logging.error(e)
else: else:
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():
from bpy.utils import register_class from bpy.utils import register_class
for cls in classes: for cls in classes:
register_class(cls) register_class(cls)
+1 -1
View File
@@ -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(
+2 -3
View File
@@ -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):