Compare commits
2 Commits
204-animat
...
46-composi
Author | SHA1 | Date | |
---|---|---|---|
c06febed45 | |||
8c3e510231 |
@ -21,7 +21,7 @@ bl_info = {
|
|||||||
"author": "Swann Martinez",
|
"author": "Swann Martinez",
|
||||||
"version": (0, 5, 0),
|
"version": (0, 5, 0),
|
||||||
"description": "Enable real-time collaborative workflow inside blender",
|
"description": "Enable real-time collaborative workflow inside blender",
|
||||||
"blender": (2, 82, 0),
|
"blender": (2, 93, 0),
|
||||||
"location": "3D View > Sidebar > Multi-User tab",
|
"location": "3D View > Sidebar > Multi-User tab",
|
||||||
"warning": "Unstable addon, use it at your own risks",
|
"warning": "Unstable addon, use it at your own risks",
|
||||||
"category": "Collaboration",
|
"category": "Collaboration",
|
||||||
@ -59,7 +59,6 @@ def register():
|
|||||||
|
|
||||||
from . import presence
|
from . import presence
|
||||||
from . import operators
|
from . import operators
|
||||||
from . import handlers
|
|
||||||
from . import ui
|
from . import ui
|
||||||
from . import preferences
|
from . import preferences
|
||||||
from . import addon_updater_ops
|
from . import addon_updater_ops
|
||||||
@ -68,7 +67,6 @@ def register():
|
|||||||
addon_updater_ops.register(bl_info)
|
addon_updater_ops.register(bl_info)
|
||||||
presence.register()
|
presence.register()
|
||||||
operators.register()
|
operators.register()
|
||||||
handlers.register()
|
|
||||||
ui.register()
|
ui.register()
|
||||||
except ModuleNotFoundError as e:
|
except ModuleNotFoundError as e:
|
||||||
raise Exception(module_error_msg)
|
raise Exception(module_error_msg)
|
||||||
@ -89,7 +87,6 @@ def register():
|
|||||||
def unregister():
|
def unregister():
|
||||||
from . import presence
|
from . import presence
|
||||||
from . import operators
|
from . import operators
|
||||||
from . import handlers
|
|
||||||
from . import ui
|
from . import ui
|
||||||
from . import preferences
|
from . import preferences
|
||||||
from . import addon_updater_ops
|
from . import addon_updater_ops
|
||||||
@ -99,7 +96,6 @@ def unregister():
|
|||||||
presence.unregister()
|
presence.unregister()
|
||||||
addon_updater_ops.unregister()
|
addon_updater_ops.unregister()
|
||||||
ui.unregister()
|
ui.unregister()
|
||||||
handlers.unregister()
|
|
||||||
operators.unregister()
|
operators.unregister()
|
||||||
preferences.unregister()
|
preferences.unregister()
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ __all__ = [
|
|||||||
'bl_node_group',
|
'bl_node_group',
|
||||||
'bl_texture',
|
'bl_texture',
|
||||||
"bl_particle",
|
"bl_particle",
|
||||||
|
# 'bl_compositor',
|
||||||
] # Order here defines execution order
|
] # Order here defines execution order
|
||||||
|
|
||||||
if bpy.app.version[1] >= 91:
|
if bpy.app.version[1] >= 91:
|
||||||
|
@ -219,7 +219,7 @@ def load_fcurve(fcurve_data, fcurve):
|
|||||||
def dump_animation_data(datablock):
|
def dump_animation_data(datablock):
|
||||||
animation_data = {}
|
animation_data = {}
|
||||||
if has_action(datablock):
|
if has_action(datablock):
|
||||||
animation_data['action'] = datablock.animation_data.action.uuid
|
animation_data['action'] = datablock.animation_data.action.name
|
||||||
if has_driver(datablock):
|
if has_driver(datablock):
|
||||||
animation_data['drivers'] = []
|
animation_data['drivers'] = []
|
||||||
for driver in datablock.animation_data.drivers:
|
for driver in datablock.animation_data.drivers:
|
||||||
@ -241,10 +241,8 @@ def load_animation_data(animation_data, datablock):
|
|||||||
for driver in animation_data['drivers']:
|
for driver in animation_data['drivers']:
|
||||||
load_driver(datablock, driver)
|
load_driver(datablock, driver)
|
||||||
|
|
||||||
action = animation_data.get('action')
|
if 'action' in animation_data:
|
||||||
if action:
|
datablock.animation_data.action = bpy.data.actions[animation_data['action']]
|
||||||
action = resolve_datablock_from_uuid(action, bpy.data.actions)
|
|
||||||
datablock.animation_data.action = action
|
|
||||||
elif datablock.animation_data.action:
|
elif datablock.animation_data.action:
|
||||||
datablock.animation_data.action = None
|
datablock.animation_data.action = None
|
||||||
|
|
||||||
@ -261,8 +259,6 @@ def resolve_animation_dependencies(datablock):
|
|||||||
|
|
||||||
|
|
||||||
class BlAction(ReplicatedDatablock):
|
class BlAction(ReplicatedDatablock):
|
||||||
use_delta = True
|
|
||||||
|
|
||||||
bl_id = "actions"
|
bl_id = "actions"
|
||||||
bl_class = bpy.types.Action
|
bl_class = bpy.types.Action
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -37,8 +37,6 @@ def get_roll(bone: bpy.types.Bone) -> float:
|
|||||||
|
|
||||||
|
|
||||||
class BlArmature(ReplicatedDatablock):
|
class BlArmature(ReplicatedDatablock):
|
||||||
use_delta = True
|
|
||||||
|
|
||||||
bl_id = "armatures"
|
bl_id = "armatures"
|
||||||
bl_class = bpy.types.Armature
|
bl_class = bpy.types.Armature
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -26,8 +26,6 @@ from .bl_action import dump_animation_data, load_animation_data, resolve_animati
|
|||||||
|
|
||||||
|
|
||||||
class BlCamera(ReplicatedDatablock):
|
class BlCamera(ReplicatedDatablock):
|
||||||
use_delta = True
|
|
||||||
|
|
||||||
bl_id = "cameras"
|
bl_id = "cameras"
|
||||||
bl_class = bpy.types.Camera
|
bl_class = bpy.types.Camera
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
81
multi_user/bl_types/bl_compositor.py
Normal file
81
multi_user/bl_types/bl_compositor.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# ##### END GPL LICENSE BLOCK #####
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import mathutils
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from .dump_anything import Loader, Dumper
|
||||||
|
from replication.protocol import ReplicatedDatablock
|
||||||
|
|
||||||
|
from .bl_datablock import get_datablock_from_uuid, resolve_datablock_from_uuid
|
||||||
|
from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies
|
||||||
|
from .node_tree import load_node_tree, dump_node_tree, get_node_tree_dependencies
|
||||||
|
|
||||||
|
class BlCompositor(ReplicatedDatablock):
|
||||||
|
bl_id = "compositor"
|
||||||
|
bl_class = bpy.types.CompositorNodeTree
|
||||||
|
bl_check_common = True
|
||||||
|
bl_icon = 'COMPOSITOR_NODE'
|
||||||
|
bl_reload_parent = False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def construct(data: dict) -> object:
|
||||||
|
return bpy.data.scenes["Scene"].node_tree # TODO: resolve_datablock_from_uuid for multiple scenes
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load(data: dict, datablock: object):
|
||||||
|
load_animation_data(data.get('animation_data'), datablock)
|
||||||
|
loader = Loader()
|
||||||
|
loader.load(datablock, data)
|
||||||
|
load_node_tree(data['node_tree'], datablock)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def dump(datablock: object) -> dict:
|
||||||
|
comp_dumper = Dumper()
|
||||||
|
comp_dumper.depth = 1
|
||||||
|
comp_dumper.include_filter = [
|
||||||
|
'use_nodes',
|
||||||
|
'name',
|
||||||
|
]
|
||||||
|
data = comp_dumper.dump(datablock)
|
||||||
|
|
||||||
|
data['node_tree'] = dump_node_tree(datablock)
|
||||||
|
|
||||||
|
data['animation_data'] = dump_animation_data(datablock)
|
||||||
|
return data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve(data: dict) -> object:
|
||||||
|
uuid = data.get('uuid')
|
||||||
|
return resolve_datablock_from_uuid(uuid, bpy.data.scenes["Scene"].node_tree)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_deps(datablock: object) -> [object]:
|
||||||
|
deps = []
|
||||||
|
|
||||||
|
deps.extend(get_node_tree_dependencies(datablock))
|
||||||
|
|
||||||
|
deps.extend(resolve_animation_dependencies(datablock))
|
||||||
|
|
||||||
|
return deps
|
||||||
|
|
||||||
|
_type = bpy.types.CompositorNodeTree
|
||||||
|
_class = BlCompositor
|
@ -137,8 +137,6 @@ SPLINE_METADATA = [
|
|||||||
|
|
||||||
|
|
||||||
class BlCurve(ReplicatedDatablock):
|
class BlCurve(ReplicatedDatablock):
|
||||||
use_delta = True
|
|
||||||
|
|
||||||
bl_id = "curves"
|
bl_id = "curves"
|
||||||
bl_class = bpy.types.Curve
|
bl_class = bpy.types.Curve
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -29,8 +29,6 @@ POINT = ['co', 'weight_softbody', 'co_deform']
|
|||||||
|
|
||||||
|
|
||||||
class BlLattice(ReplicatedDatablock):
|
class BlLattice(ReplicatedDatablock):
|
||||||
use_delta = True
|
|
||||||
|
|
||||||
bl_id = "lattices"
|
bl_id = "lattices"
|
||||||
bl_class = bpy.types.Lattice
|
bl_class = bpy.types.Lattice
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -26,8 +26,6 @@ from .bl_action import dump_animation_data, load_animation_data, resolve_animati
|
|||||||
|
|
||||||
|
|
||||||
class BlLight(ReplicatedDatablock):
|
class BlLight(ReplicatedDatablock):
|
||||||
use_delta = True
|
|
||||||
|
|
||||||
bl_id = "lights"
|
bl_id = "lights"
|
||||||
bl_class = bpy.types.Light
|
bl_class = bpy.types.Light
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -25,8 +25,6 @@ from replication.protocol import ReplicatedDatablock
|
|||||||
from .bl_datablock import resolve_datablock_from_uuid
|
from .bl_datablock import resolve_datablock_from_uuid
|
||||||
|
|
||||||
class BlLightprobe(ReplicatedDatablock):
|
class BlLightprobe(ReplicatedDatablock):
|
||||||
use_delta = True
|
|
||||||
|
|
||||||
bl_id = "lightprobes"
|
bl_id = "lightprobes"
|
||||||
bl_class = bpy.types.LightProbe
|
bl_class = bpy.types.LightProbe
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -28,342 +28,7 @@ from replication.protocol import ReplicatedDatablock
|
|||||||
|
|
||||||
from .bl_datablock import get_datablock_from_uuid, resolve_datablock_from_uuid
|
from .bl_datablock import get_datablock_from_uuid, resolve_datablock_from_uuid
|
||||||
from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies
|
from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies
|
||||||
|
from .node_tree import load_node_tree, dump_node_tree, get_node_tree_dependencies
|
||||||
NODE_SOCKET_INDEX = re.compile('\[(\d*)\]')
|
|
||||||
IGNORED_SOCKETS = ['GEOMETRY', 'SHADER', 'CUSTOM']
|
|
||||||
|
|
||||||
def load_node(node_data: dict, node_tree: bpy.types.ShaderNodeTree):
|
|
||||||
""" Load a node into a node_tree from a dict
|
|
||||||
|
|
||||||
:arg node_data: dumped node data
|
|
||||||
:type node_data: dict
|
|
||||||
:arg node_tree: target node_tree
|
|
||||||
:type node_tree: bpy.types.NodeTree
|
|
||||||
"""
|
|
||||||
loader = Loader()
|
|
||||||
target_node = node_tree.nodes.new(type=node_data["bl_idname"])
|
|
||||||
target_node.select = False
|
|
||||||
loader.load(target_node, node_data)
|
|
||||||
image_uuid = node_data.get('image_uuid', None)
|
|
||||||
node_tree_uuid = node_data.get('node_tree_uuid', None)
|
|
||||||
|
|
||||||
if image_uuid and not target_node.image:
|
|
||||||
image = resolve_datablock_from_uuid(image_uuid, bpy.data.images)
|
|
||||||
if image is None:
|
|
||||||
logging.error(f"Fail to find material image from uuid {image_uuid}")
|
|
||||||
else:
|
|
||||||
target_node.image = image
|
|
||||||
|
|
||||||
if node_tree_uuid:
|
|
||||||
target_node.node_tree = get_datablock_from_uuid(node_tree_uuid, None)
|
|
||||||
|
|
||||||
inputs_data = node_data.get('inputs')
|
|
||||||
if inputs_data:
|
|
||||||
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"):
|
|
||||||
loaded_input = inputs_data[idx]
|
|
||||||
try:
|
|
||||||
if inpt.type in ['OBJECT', 'COLLECTION']:
|
|
||||||
inpt.default_value = get_datablock_from_uuid(loaded_input, None)
|
|
||||||
else:
|
|
||||||
inpt.default_value = loaded_input
|
|
||||||
except Exception as e:
|
|
||||||
logging.warning(f"Node {target_node.name} input {inpt.name} parameter not supported, skipping ({e})")
|
|
||||||
else:
|
|
||||||
logging.warning(f"Node {target_node.name} input length mismatch.")
|
|
||||||
|
|
||||||
outputs_data = node_data.get('outputs')
|
|
||||||
if outputs_data:
|
|
||||||
outputs = [o for o in target_node.outputs if o.type not in IGNORED_SOCKETS]
|
|
||||||
for idx, output in enumerate(outputs):
|
|
||||||
if idx < len(outputs_data) and hasattr(output, "default_value"):
|
|
||||||
loaded_output = outputs_data[idx]
|
|
||||||
try:
|
|
||||||
if output.type in ['OBJECT', 'COLLECTION']:
|
|
||||||
output.default_value = get_datablock_from_uuid(loaded_output, None)
|
|
||||||
else:
|
|
||||||
output.default_value = loaded_output
|
|
||||||
except Exception as e:
|
|
||||||
logging.warning(
|
|
||||||
f"Node {target_node.name} output {output.name} parameter not supported, skipping ({e})")
|
|
||||||
else:
|
|
||||||
logging.warning(
|
|
||||||
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):
|
|
||||||
""" Load node_tree links from a list
|
|
||||||
|
|
||||||
:arg links_data: dumped node links
|
|
||||||
:type links_data: list
|
|
||||||
:arg node_tree: node links collection
|
|
||||||
:type node_tree: bpy.types.NodeTree
|
|
||||||
"""
|
|
||||||
|
|
||||||
for link in links_data:
|
|
||||||
input_socket = node_tree.nodes[link['to_node']
|
|
||||||
].inputs[int(link['to_socket'])]
|
|
||||||
output_socket = node_tree.nodes[link['from_node']].outputs[int(
|
|
||||||
link['from_socket'])]
|
|
||||||
node_tree.links.new(input_socket, output_socket)
|
|
||||||
|
|
||||||
|
|
||||||
def dump_links(links):
|
|
||||||
""" Dump node_tree links collection to a list
|
|
||||||
|
|
||||||
:arg links: node links collection
|
|
||||||
:type links: bpy.types.NodeLinks
|
|
||||||
:retrun: list
|
|
||||||
"""
|
|
||||||
|
|
||||||
links_data = []
|
|
||||||
|
|
||||||
for link in links:
|
|
||||||
to_socket = NODE_SOCKET_INDEX.search(
|
|
||||||
link.to_socket.path_from_id()).group(1)
|
|
||||||
from_socket = NODE_SOCKET_INDEX.search(
|
|
||||||
link.from_socket.path_from_id()).group(1)
|
|
||||||
links_data.append({
|
|
||||||
'to_node': link.to_node.name,
|
|
||||||
'to_socket': to_socket,
|
|
||||||
'from_node': link.from_node.name,
|
|
||||||
'from_socket': from_socket,
|
|
||||||
})
|
|
||||||
|
|
||||||
return links_data
|
|
||||||
|
|
||||||
|
|
||||||
def dump_node_tree(node_tree: bpy.types.ShaderNodeTree) -> dict:
|
|
||||||
""" Dump a shader node_tree to a dict including links and nodes
|
|
||||||
|
|
||||||
:arg node_tree: dumped shader node tree
|
|
||||||
:type node_tree: bpy.types.ShaderNodeTree
|
|
||||||
:return: dict
|
|
||||||
"""
|
|
||||||
node_tree_data = {
|
|
||||||
'nodes': {node.name: dump_node(node) for node in node_tree.nodes},
|
|
||||||
'links': dump_links(node_tree.links),
|
|
||||||
'name': node_tree.name,
|
|
||||||
'type': type(node_tree).__name__
|
|
||||||
}
|
|
||||||
|
|
||||||
for socket_id in ['inputs', 'outputs']:
|
|
||||||
socket_collection = getattr(node_tree, socket_id)
|
|
||||||
node_tree_data[socket_id] = dump_node_tree_sockets(socket_collection)
|
|
||||||
|
|
||||||
return node_tree_data
|
|
||||||
|
|
||||||
|
|
||||||
def dump_node_tree_sockets(sockets: bpy.types.Collection) -> dict:
|
|
||||||
""" dump sockets of a shader_node_tree
|
|
||||||
|
|
||||||
:arg target_node_tree: target node_tree
|
|
||||||
:type target_node_tree: bpy.types.NodeTree
|
|
||||||
:arg socket_id: socket identifer
|
|
||||||
:type socket_id: str
|
|
||||||
:return: dict
|
|
||||||
"""
|
|
||||||
sockets_data = []
|
|
||||||
for socket in sockets:
|
|
||||||
try:
|
|
||||||
socket_uuid = socket['uuid']
|
|
||||||
except Exception:
|
|
||||||
socket_uuid = str(uuid4())
|
|
||||||
socket['uuid'] = socket_uuid
|
|
||||||
|
|
||||||
sockets_data.append((socket.name, socket.bl_socket_idname, socket_uuid))
|
|
||||||
|
|
||||||
return sockets_data
|
|
||||||
|
|
||||||
|
|
||||||
def load_node_tree_sockets(sockets: bpy.types.Collection,
|
|
||||||
sockets_data: dict):
|
|
||||||
""" load sockets of a shader_node_tree
|
|
||||||
|
|
||||||
:arg target_node_tree: target node_tree
|
|
||||||
:type target_node_tree: bpy.types.NodeTree
|
|
||||||
:arg socket_id: socket identifer
|
|
||||||
:type socket_id: str
|
|
||||||
:arg socket_data: dumped socket data
|
|
||||||
:type socket_data: dict
|
|
||||||
"""
|
|
||||||
# Check for removed sockets
|
|
||||||
for socket in sockets:
|
|
||||||
if not [s for s in sockets_data if 'uuid' in socket and socket['uuid'] == s[2]]:
|
|
||||||
sockets.remove(socket)
|
|
||||||
|
|
||||||
# Check for new sockets
|
|
||||||
for idx, socket_data in enumerate(sockets_data):
|
|
||||||
try:
|
|
||||||
checked_socket = sockets[idx]
|
|
||||||
if checked_socket.name != socket_data[0]:
|
|
||||||
checked_socket.name = socket_data[0]
|
|
||||||
except Exception:
|
|
||||||
s = sockets.new(socket_data[1], socket_data[0])
|
|
||||||
s['uuid'] = socket_data[2]
|
|
||||||
|
|
||||||
|
|
||||||
def load_node_tree(node_tree_data: dict, target_node_tree: bpy.types.ShaderNodeTree) -> dict:
|
|
||||||
"""Load a shader node_tree from dumped data
|
|
||||||
|
|
||||||
:arg node_tree_data: dumped node data
|
|
||||||
:type node_tree_data: dict
|
|
||||||
:arg target_node_tree: target node_tree
|
|
||||||
:type target_node_tree: bpy.types.NodeTree
|
|
||||||
"""
|
|
||||||
# TODO: load only required nodes
|
|
||||||
target_node_tree.nodes.clear()
|
|
||||||
|
|
||||||
if not target_node_tree.is_property_readonly('name'):
|
|
||||||
target_node_tree.name = node_tree_data['name']
|
|
||||||
|
|
||||||
if 'inputs' in node_tree_data:
|
|
||||||
socket_collection = getattr(target_node_tree, 'inputs')
|
|
||||||
load_node_tree_sockets(socket_collection, node_tree_data['inputs'])
|
|
||||||
|
|
||||||
if 'outputs' in node_tree_data:
|
|
||||||
socket_collection = getattr(target_node_tree, 'outputs')
|
|
||||||
load_node_tree_sockets(socket_collection, node_tree_data['outputs'])
|
|
||||||
|
|
||||||
# Load nodes
|
|
||||||
for node in node_tree_data["nodes"]:
|
|
||||||
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
|
|
||||||
# Load nodes links
|
|
||||||
target_node_tree.links.clear()
|
|
||||||
|
|
||||||
load_links(node_tree_data["links"], target_node_tree)
|
|
||||||
|
|
||||||
|
|
||||||
def get_node_tree_dependencies(node_tree: bpy.types.NodeTree) -> list:
|
|
||||||
def has_image(node): return (
|
|
||||||
node.type in ['TEX_IMAGE', 'TEX_ENVIRONMENT'] and node.image)
|
|
||||||
|
|
||||||
def has_node_group(node): return (
|
|
||||||
hasattr(node, 'node_tree') and node.node_tree)
|
|
||||||
|
|
||||||
def has_texture(node): return (
|
|
||||||
node.type in ['ATTRIBUTE_SAMPLE_TEXTURE','TEXTURE'] and node.texture)
|
|
||||||
deps = []
|
|
||||||
|
|
||||||
for node in node_tree.nodes:
|
|
||||||
if has_image(node):
|
|
||||||
deps.append(node.image)
|
|
||||||
elif has_node_group(node):
|
|
||||||
deps.append(node.node_tree)
|
|
||||||
elif has_texture(node):
|
|
||||||
deps.append(node.texture)
|
|
||||||
|
|
||||||
return deps
|
|
||||||
|
|
||||||
|
|
||||||
def dump_materials_slots(materials: bpy.types.bpy_prop_collection) -> list:
|
def dump_materials_slots(materials: bpy.types.bpy_prop_collection) -> list:
|
||||||
""" Dump material slots collection
|
""" Dump material slots collection
|
||||||
@ -397,14 +62,11 @@ def load_materials_slots(src_materials: list, dst_materials: bpy.types.bpy_prop_
|
|||||||
|
|
||||||
|
|
||||||
class BlMaterial(ReplicatedDatablock):
|
class BlMaterial(ReplicatedDatablock):
|
||||||
use_delta = True
|
|
||||||
|
|
||||||
bl_id = "materials"
|
bl_id = "materials"
|
||||||
bl_class = bpy.types.Material
|
bl_class = bpy.types.Material
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
bl_icon = 'MATERIAL_DATA'
|
bl_icon = 'MATERIAL_DATA'
|
||||||
bl_reload_parent = False
|
bl_reload_parent = False
|
||||||
bl_reload_child = True
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def construct(data: dict) -> object:
|
def construct(data: dict) -> object:
|
||||||
@ -412,6 +74,8 @@ class BlMaterial(ReplicatedDatablock):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load(data: dict, datablock: object):
|
def load(data: dict, datablock: object):
|
||||||
|
load_animation_data(data.get('animation_data'), datablock)
|
||||||
|
|
||||||
loader = Loader()
|
loader = Loader()
|
||||||
|
|
||||||
is_grease_pencil = data.get('is_grease_pencil')
|
is_grease_pencil = data.get('is_grease_pencil')
|
||||||
@ -428,8 +92,6 @@ class BlMaterial(ReplicatedDatablock):
|
|||||||
datablock.use_nodes = True
|
datablock.use_nodes = True
|
||||||
|
|
||||||
load_node_tree(data['node_tree'], datablock.node_tree)
|
load_node_tree(data['node_tree'], datablock.node_tree)
|
||||||
load_animation_data(data.get('nodes_animation_data'), datablock.node_tree)
|
|
||||||
load_animation_data(data.get('animation_data'), datablock)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def dump(datablock: object) -> dict:
|
def dump(datablock: object) -> dict:
|
||||||
@ -497,10 +159,8 @@ class BlMaterial(ReplicatedDatablock):
|
|||||||
data['grease_pencil'] = gp_mat_dumper.dump(datablock.grease_pencil)
|
data['grease_pencil'] = gp_mat_dumper.dump(datablock.grease_pencil)
|
||||||
elif datablock.use_nodes:
|
elif datablock.use_nodes:
|
||||||
data['node_tree'] = dump_node_tree(datablock.node_tree)
|
data['node_tree'] = dump_node_tree(datablock.node_tree)
|
||||||
data['nodes_animation_data'] = dump_animation_data(datablock.node_tree)
|
|
||||||
|
|
||||||
data['animation_data'] = dump_animation_data(datablock)
|
data['animation_data'] = dump_animation_data(datablock)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -514,7 +174,7 @@ class BlMaterial(ReplicatedDatablock):
|
|||||||
|
|
||||||
if datablock.use_nodes:
|
if datablock.use_nodes:
|
||||||
deps.extend(get_node_tree_dependencies(datablock.node_tree))
|
deps.extend(get_node_tree_dependencies(datablock.node_tree))
|
||||||
deps.extend(resolve_animation_dependencies(datablock.node_tree))
|
|
||||||
deps.extend(resolve_animation_dependencies(datablock))
|
deps.extend(resolve_animation_dependencies(datablock))
|
||||||
|
|
||||||
return deps
|
return deps
|
||||||
|
@ -55,8 +55,6 @@ POLYGON = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
class BlMesh(ReplicatedDatablock):
|
class BlMesh(ReplicatedDatablock):
|
||||||
use_delta = True
|
|
||||||
|
|
||||||
bl_id = "meshes"
|
bl_id = "meshes"
|
||||||
bl_class = bpy.types.Mesh
|
bl_class = bpy.types.Mesh
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -65,8 +65,6 @@ def load_metaball_elements(elements_data, elements):
|
|||||||
|
|
||||||
|
|
||||||
class BlMetaball(ReplicatedDatablock):
|
class BlMetaball(ReplicatedDatablock):
|
||||||
use_delta = True
|
|
||||||
|
|
||||||
bl_id = "metaballs"
|
bl_id = "metaballs"
|
||||||
bl_class = bpy.types.MetaBall
|
bl_class = bpy.types.MetaBall
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -28,8 +28,6 @@ from .bl_datablock import resolve_datablock_from_uuid
|
|||||||
from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies
|
from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies
|
||||||
|
|
||||||
class BlNodeGroup(ReplicatedDatablock):
|
class BlNodeGroup(ReplicatedDatablock):
|
||||||
use_delta = True
|
|
||||||
|
|
||||||
bl_id = "node_groups"
|
bl_id = "node_groups"
|
||||||
bl_class = bpy.types.NodeTree
|
bl_class = bpy.types.NodeTree
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -24,7 +24,7 @@ from replication.exception import ContextError
|
|||||||
|
|
||||||
from replication.protocol import ReplicatedDatablock
|
from replication.protocol import ReplicatedDatablock
|
||||||
from .bl_datablock import get_datablock_from_uuid, resolve_datablock_from_uuid
|
from .bl_datablock import get_datablock_from_uuid, resolve_datablock_from_uuid
|
||||||
from .bl_material import IGNORED_SOCKETS
|
from .node_tree import IGNORED_SOCKETS
|
||||||
from ..utils import get_preferences
|
from ..utils import get_preferences
|
||||||
from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies
|
from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies
|
||||||
from .dump_anything import (
|
from .dump_anything import (
|
||||||
@ -493,8 +493,6 @@ def load_modifiers_custom_data(dumped_modifiers: dict, modifiers: bpy.types.bpy_
|
|||||||
|
|
||||||
|
|
||||||
class BlObject(ReplicatedDatablock):
|
class BlObject(ReplicatedDatablock):
|
||||||
use_delta = True
|
|
||||||
|
|
||||||
bl_id = "objects"
|
bl_id = "objects"
|
||||||
bl_class = bpy.types.Object
|
bl_class = bpy.types.Object
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -3,8 +3,7 @@ import mathutils
|
|||||||
|
|
||||||
from . import dump_anything
|
from . import dump_anything
|
||||||
from replication.protocol import ReplicatedDatablock
|
from replication.protocol import ReplicatedDatablock
|
||||||
from .bl_datablock import get_datablock_from_uuid
|
from .bl_datablock import get_datablock_from_uuid, resolve_datablock_from_uuid
|
||||||
from .bl_datablock import resolve_datablock_from_uuid
|
|
||||||
from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies
|
from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies
|
||||||
|
|
||||||
|
|
||||||
@ -41,8 +40,6 @@ IGNORED_ATTR = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
class BlParticle(ReplicatedDatablock):
|
class BlParticle(ReplicatedDatablock):
|
||||||
use_delta = True
|
|
||||||
|
|
||||||
bl_id = "particles"
|
bl_id = "particles"
|
||||||
bl_class = bpy.types.ParticleSettings
|
bl_class = bpy.types.ParticleSettings
|
||||||
bl_icon = "PARTICLES"
|
bl_icon = "PARTICLES"
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
import re
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import mathutils
|
import mathutils
|
||||||
@ -29,10 +30,12 @@ from replication.protocol import ReplicatedDatablock
|
|||||||
from ..utils import flush_history, get_preferences
|
from ..utils import flush_history, get_preferences
|
||||||
from .bl_action import (dump_animation_data, load_animation_data,
|
from .bl_action import (dump_animation_data, load_animation_data,
|
||||||
resolve_animation_dependencies)
|
resolve_animation_dependencies)
|
||||||
|
from .node_tree import (get_node_tree_dependencies, load_node_tree,
|
||||||
|
dump_node_tree)
|
||||||
from .bl_collection import (dump_collection_children, dump_collection_objects,
|
from .bl_collection import (dump_collection_children, dump_collection_objects,
|
||||||
load_collection_childrens, load_collection_objects,
|
load_collection_childrens, load_collection_objects,
|
||||||
resolve_collection_dependencies)
|
resolve_collection_dependencies)
|
||||||
from .bl_datablock import resolve_datablock_from_uuid
|
from .bl_datablock import get_datablock_from_uuid, resolve_datablock_from_uuid
|
||||||
from .bl_file import get_filepath
|
from .bl_file import get_filepath
|
||||||
from .dump_anything import Dumper, Loader
|
from .dump_anything import Dumper, Loader
|
||||||
|
|
||||||
@ -303,7 +306,6 @@ def dump_sequence(sequence: bpy.types.Sequence) -> dict:
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def load_sequence(sequence_data: dict,
|
def load_sequence(sequence_data: dict,
|
||||||
sequence_editor: bpy.types.SequenceEditor):
|
sequence_editor: bpy.types.SequenceEditor):
|
||||||
""" Load sequence from dumped data
|
""" Load sequence from dumped data
|
||||||
@ -370,7 +372,6 @@ def load_sequence(sequence_data: dict,
|
|||||||
loader.load(sequence, sequence_data)
|
loader.load(sequence, sequence_data)
|
||||||
sequence.select = False
|
sequence.select = False
|
||||||
|
|
||||||
|
|
||||||
class BlScene(ReplicatedDatablock):
|
class BlScene(ReplicatedDatablock):
|
||||||
is_root = True
|
is_root = True
|
||||||
use_delta = True
|
use_delta = True
|
||||||
@ -447,6 +448,14 @@ class BlScene(ReplicatedDatablock):
|
|||||||
|
|
||||||
# FIXME: Find a better way after the replication big refacotoring
|
# FIXME: Find a better way after the replication big refacotoring
|
||||||
# Keep other user from deleting collection object by flushing their history
|
# Keep other user from deleting collection object by flushing their history
|
||||||
|
|
||||||
|
# Compositor
|
||||||
|
if data["use_nodes"]:
|
||||||
|
if datablock.node_tree is None:
|
||||||
|
datablock.use_nodes = True
|
||||||
|
|
||||||
|
load_node_tree(data['node_tree'], datablock.node_tree)
|
||||||
|
|
||||||
flush_history()
|
flush_history()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -458,6 +467,7 @@ class BlScene(ReplicatedDatablock):
|
|||||||
scene_dumper = Dumper()
|
scene_dumper = Dumper()
|
||||||
scene_dumper.depth = 1
|
scene_dumper.depth = 1
|
||||||
scene_dumper.include_filter = [
|
scene_dumper.include_filter = [
|
||||||
|
'use_nodes',
|
||||||
'name',
|
'name',
|
||||||
'world',
|
'world',
|
||||||
'id',
|
'id',
|
||||||
@ -517,6 +527,11 @@ class BlScene(ReplicatedDatablock):
|
|||||||
dumped_sequences[seq.name] = dump_sequence(seq)
|
dumped_sequences[seq.name] = dump_sequence(seq)
|
||||||
data['sequences'] = dumped_sequences
|
data['sequences'] = dumped_sequences
|
||||||
|
|
||||||
|
# Compositor
|
||||||
|
if datablock.use_nodes:
|
||||||
|
data['node_tree'] = dump_node_tree(datablock.node_tree)
|
||||||
|
data['animation_data'] = dump_animation_data(datablock)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -550,6 +565,12 @@ class BlScene(ReplicatedDatablock):
|
|||||||
Path(bpy.path.abspath(sequence.directory),
|
Path(bpy.path.abspath(sequence.directory),
|
||||||
elem.filename))
|
elem.filename))
|
||||||
|
|
||||||
|
# Compositor
|
||||||
|
if datablock.use_nodes:
|
||||||
|
deps.extend(get_node_tree_dependencies(datablock.node_tree))
|
||||||
|
|
||||||
|
deps.extend(resolve_animation_dependencies(datablock))
|
||||||
|
|
||||||
return deps
|
return deps
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -25,8 +25,6 @@ from .bl_datablock import resolve_datablock_from_uuid
|
|||||||
from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies
|
from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies
|
||||||
|
|
||||||
class BlSpeaker(ReplicatedDatablock):
|
class BlSpeaker(ReplicatedDatablock):
|
||||||
use_delta = True
|
|
||||||
|
|
||||||
bl_id = "speakers"
|
bl_id = "speakers"
|
||||||
bl_class = bpy.types.Speaker
|
bl_class = bpy.types.Speaker
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -26,8 +26,6 @@ from .bl_action import dump_animation_data, load_animation_data, resolve_animati
|
|||||||
import bpy.types as T
|
import bpy.types as T
|
||||||
|
|
||||||
class BlTexture(ReplicatedDatablock):
|
class BlTexture(ReplicatedDatablock):
|
||||||
use_delta = True
|
|
||||||
|
|
||||||
bl_id = "textures"
|
bl_id = "textures"
|
||||||
bl_class = bpy.types.Texture
|
bl_class = bpy.types.Texture
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -27,8 +27,6 @@ from .bl_material import dump_materials_slots, load_materials_slots
|
|||||||
from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies
|
from .bl_action import dump_animation_data, load_animation_data, resolve_animation_dependencies
|
||||||
|
|
||||||
class BlVolume(ReplicatedDatablock):
|
class BlVolume(ReplicatedDatablock):
|
||||||
use_delta = True
|
|
||||||
|
|
||||||
bl_id = "volumes"
|
bl_id = "volumes"
|
||||||
bl_class = bpy.types.Volume
|
bl_class = bpy.types.Volume
|
||||||
bl_check_common = False
|
bl_check_common = False
|
||||||
|
@ -21,7 +21,7 @@ import mathutils
|
|||||||
|
|
||||||
from .dump_anything import Loader, Dumper
|
from .dump_anything import Loader, Dumper
|
||||||
from replication.protocol import ReplicatedDatablock
|
from replication.protocol import ReplicatedDatablock
|
||||||
from .bl_material import (load_node_tree,
|
from .node_tree import (load_node_tree,
|
||||||
dump_node_tree,
|
dump_node_tree,
|
||||||
get_node_tree_dependencies)
|
get_node_tree_dependencies)
|
||||||
|
|
||||||
@ -30,8 +30,6 @@ from .bl_action import dump_animation_data, load_animation_data, resolve_animati
|
|||||||
|
|
||||||
|
|
||||||
class BlWorld(ReplicatedDatablock):
|
class BlWorld(ReplicatedDatablock):
|
||||||
use_delta = True
|
|
||||||
|
|
||||||
bl_id = "worlds"
|
bl_id = "worlds"
|
||||||
bl_class = bpy.types.World
|
bl_class = bpy.types.World
|
||||||
bl_check_common = True
|
bl_check_common = True
|
||||||
|
362
multi_user/bl_types/node_tree.py
Normal file
362
multi_user/bl_types/node_tree.py
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# ##### END GPL LICENSE BLOCK #####
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import mathutils
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from .dump_anything import Loader, Dumper
|
||||||
|
|
||||||
|
from .bl_datablock import get_datablock_from_uuid, resolve_datablock_from_uuid
|
||||||
|
|
||||||
|
IGNORED_SOCKETS = ['GEOMETRY', 'SHADER', 'CUSTOM']
|
||||||
|
NODE_SOCKET_INDEX = re.compile('\[(\d*)\]')
|
||||||
|
|
||||||
|
def load_node(node_data: dict, node_tree: bpy.types.NodeTree):
|
||||||
|
""" Load a node into a node_tree from a dict
|
||||||
|
|
||||||
|
:arg node_data: dumped node data
|
||||||
|
:type node_data: dict
|
||||||
|
:arg node_tree: target node_tree
|
||||||
|
:type node_tree: bpy.types.NodeTree
|
||||||
|
"""
|
||||||
|
loader = Loader()
|
||||||
|
target_node = node_tree.nodes.new(type=node_data["bl_idname"])
|
||||||
|
target_node.select = False
|
||||||
|
loader.load(target_node, node_data)
|
||||||
|
image_uuid = node_data.get('image_uuid', None)
|
||||||
|
node_tree_uuid = node_data.get('node_tree_uuid', None)
|
||||||
|
|
||||||
|
if image_uuid and not target_node.image:
|
||||||
|
image = resolve_datablock_from_uuid(image_uuid, bpy.data.images)
|
||||||
|
if image is None:
|
||||||
|
logging.error(f"Fail to find material image from uuid {image_uuid}")
|
||||||
|
else:
|
||||||
|
target_node.image = image
|
||||||
|
|
||||||
|
if node_tree_uuid:
|
||||||
|
target_node.node_tree = get_datablock_from_uuid(node_tree_uuid, None)
|
||||||
|
|
||||||
|
inputs_data = node_data.get('inputs')
|
||||||
|
if inputs_data:
|
||||||
|
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"):
|
||||||
|
loaded_input = inputs_data[idx]
|
||||||
|
try:
|
||||||
|
if inpt.type in ['OBJECT', 'COLLECTION']:
|
||||||
|
inpt.default_value = get_datablock_from_uuid(loaded_input, None)
|
||||||
|
else:
|
||||||
|
inpt.default_value = loaded_input
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"Node {target_node.name} input {inpt.name} parameter not supported, skipping ({e})")
|
||||||
|
else:
|
||||||
|
logging.warning(f"Node {target_node.name} input length mismatch.")
|
||||||
|
|
||||||
|
outputs_data = node_data.get('outputs')
|
||||||
|
if outputs_data:
|
||||||
|
outputs = [o for o in target_node.outputs if o.type not in IGNORED_SOCKETS]
|
||||||
|
for idx, output in enumerate(outputs):
|
||||||
|
if idx < len(outputs_data) and hasattr(output, "default_value"):
|
||||||
|
loaded_output = outputs_data[idx]
|
||||||
|
try:
|
||||||
|
if output.type in ['OBJECT', 'COLLECTION']:
|
||||||
|
output.default_value = get_datablock_from_uuid(loaded_output, None)
|
||||||
|
else:
|
||||||
|
output.default_value = loaded_output
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(
|
||||||
|
f"Node {target_node.name} output {output.name} parameter not supported, skipping ({e})")
|
||||||
|
else:
|
||||||
|
logging.warning(
|
||||||
|
f"Node {target_node.name} output length mismatch.")
|
||||||
|
|
||||||
|
|
||||||
|
def dump_node(node: bpy.types.Node) -> 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):
|
||||||
|
""" Load node_tree links from a list
|
||||||
|
|
||||||
|
:arg links_data: dumped node links
|
||||||
|
:type links_data: list
|
||||||
|
:arg node_tree: node links collection
|
||||||
|
:type node_tree: bpy.types.NodeTree
|
||||||
|
"""
|
||||||
|
|
||||||
|
for link in links_data:
|
||||||
|
input_socket = node_tree.nodes[link['to_node']
|
||||||
|
].inputs[int(link['to_socket'])]
|
||||||
|
output_socket = node_tree.nodes[link['from_node']].outputs[int(
|
||||||
|
link['from_socket'])]
|
||||||
|
node_tree.links.new(input_socket, output_socket)
|
||||||
|
|
||||||
|
|
||||||
|
def dump_links(links):
|
||||||
|
""" Dump node_tree links collection to a list
|
||||||
|
|
||||||
|
:arg links: node links collection
|
||||||
|
:type links: bpy.types.NodeLinks
|
||||||
|
:retrun: list
|
||||||
|
"""
|
||||||
|
|
||||||
|
links_data = []
|
||||||
|
|
||||||
|
for link in links:
|
||||||
|
to_socket = NODE_SOCKET_INDEX.search(
|
||||||
|
link.to_socket.path_from_id()).group(1)
|
||||||
|
from_socket = NODE_SOCKET_INDEX.search(
|
||||||
|
link.from_socket.path_from_id()).group(1)
|
||||||
|
links_data.append({
|
||||||
|
'to_node': link.to_node.name,
|
||||||
|
'to_socket': to_socket,
|
||||||
|
'from_node': link.from_node.name,
|
||||||
|
'from_socket': from_socket,
|
||||||
|
})
|
||||||
|
|
||||||
|
return links_data
|
||||||
|
|
||||||
|
|
||||||
|
def dump_node_tree(node_tree: bpy.types.NodeTree) -> dict:
|
||||||
|
""" Dump a node_tree to a dict including links and nodes
|
||||||
|
|
||||||
|
:arg node_tree: dumped node tree
|
||||||
|
:type node_tree: bpy.types.NodeTree
|
||||||
|
:return: dict
|
||||||
|
"""
|
||||||
|
node_tree_data = {
|
||||||
|
'nodes': {node.name: dump_node(node) for node in node_tree.nodes},
|
||||||
|
'links': dump_links(node_tree.links),
|
||||||
|
'name': node_tree.name,
|
||||||
|
'type': type(node_tree).__name__
|
||||||
|
}
|
||||||
|
|
||||||
|
for socket_id in ['inputs', 'outputs']:
|
||||||
|
socket_collection = getattr(node_tree, socket_id)
|
||||||
|
node_tree_data[socket_id] = dump_node_tree_sockets(socket_collection)
|
||||||
|
|
||||||
|
return node_tree_data
|
||||||
|
|
||||||
|
|
||||||
|
def dump_node_tree_sockets(sockets: bpy.types.Collection) -> dict:
|
||||||
|
""" dump sockets of a shader_node_tree
|
||||||
|
|
||||||
|
:arg target_node_tree: target node_tree
|
||||||
|
:type target_node_tree: bpy.types.NodeTree
|
||||||
|
:arg socket_id: socket identifer
|
||||||
|
:type socket_id: str
|
||||||
|
:return: dict
|
||||||
|
"""
|
||||||
|
sockets_data = []
|
||||||
|
for socket in sockets:
|
||||||
|
try:
|
||||||
|
socket_uuid = socket['uuid']
|
||||||
|
except Exception:
|
||||||
|
socket_uuid = str(uuid4())
|
||||||
|
socket['uuid'] = socket_uuid
|
||||||
|
|
||||||
|
sockets_data.append((socket.name, socket.bl_socket_idname, socket_uuid))
|
||||||
|
|
||||||
|
return sockets_data
|
||||||
|
|
||||||
|
|
||||||
|
def load_node_tree_sockets(sockets: bpy.types.Collection,
|
||||||
|
sockets_data: dict):
|
||||||
|
""" load sockets of a shader_node_tree
|
||||||
|
|
||||||
|
:arg target_node_tree: target node_tree
|
||||||
|
:type target_node_tree: bpy.types.NodeTree
|
||||||
|
:arg socket_id: socket identifer
|
||||||
|
:type socket_id: str
|
||||||
|
:arg socket_data: dumped socket data
|
||||||
|
:type socket_data: dict
|
||||||
|
"""
|
||||||
|
# Check for removed sockets
|
||||||
|
for socket in sockets:
|
||||||
|
if not [s for s in sockets_data if 'uuid' in socket and socket['uuid'] == s[2]]:
|
||||||
|
sockets.remove(socket)
|
||||||
|
|
||||||
|
# Check for new sockets
|
||||||
|
for idx, socket_data in enumerate(sockets_data):
|
||||||
|
try:
|
||||||
|
checked_socket = sockets[idx]
|
||||||
|
if checked_socket.name != socket_data[0]:
|
||||||
|
checked_socket.name = socket_data[0]
|
||||||
|
except Exception:
|
||||||
|
s = sockets.new(socket_data[1], socket_data[0])
|
||||||
|
s['uuid'] = socket_data[2]
|
||||||
|
|
||||||
|
|
||||||
|
def load_node_tree(node_tree_data: dict, target_node_tree: bpy.types.NodeTree) -> dict:
|
||||||
|
"""Load a shader node_tree from dumped data
|
||||||
|
|
||||||
|
:arg node_tree_data: dumped node data
|
||||||
|
:type node_tree_data: dict
|
||||||
|
:arg target_node_tree: target node_tree
|
||||||
|
:type target_node_tree: bpy.types.NodeTree
|
||||||
|
"""
|
||||||
|
# TODO: load only required nodes
|
||||||
|
target_node_tree.nodes.clear()
|
||||||
|
|
||||||
|
if not target_node_tree.is_property_readonly('name'):
|
||||||
|
target_node_tree.name = node_tree_data['name']
|
||||||
|
|
||||||
|
if 'inputs' in node_tree_data:
|
||||||
|
socket_collection = getattr(target_node_tree, 'inputs')
|
||||||
|
load_node_tree_sockets(socket_collection, node_tree_data['inputs'])
|
||||||
|
|
||||||
|
if 'outputs' in node_tree_data:
|
||||||
|
socket_collection = getattr(target_node_tree, 'outputs')
|
||||||
|
load_node_tree_sockets(socket_collection, node_tree_data['outputs'])
|
||||||
|
|
||||||
|
# Load nodes
|
||||||
|
for node in node_tree_data["nodes"]:
|
||||||
|
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
|
||||||
|
# Load nodes links
|
||||||
|
target_node_tree.links.clear()
|
||||||
|
|
||||||
|
load_links(node_tree_data["links"], target_node_tree)
|
||||||
|
|
||||||
|
|
||||||
|
def get_node_tree_dependencies(node_tree: bpy.types.NodeTree) -> list:
|
||||||
|
def has_image(node): return (
|
||||||
|
node.type in ['TEX_IMAGE', 'TEX_ENVIRONMENT','IMAGE','R_LAYER'] and node.image)
|
||||||
|
|
||||||
|
def has_node_group(node): return (
|
||||||
|
hasattr(node, 'node_tree') and node.node_tree)
|
||||||
|
|
||||||
|
def has_texture(node): return (
|
||||||
|
node.type in ['ATTRIBUTE_SAMPLE_TEXTURE','TEXTURE'] and node.texture)
|
||||||
|
deps = []
|
||||||
|
|
||||||
|
for node in node_tree.nodes:
|
||||||
|
if has_image(node):
|
||||||
|
deps.append(node.image)
|
||||||
|
elif has_node_group(node):
|
||||||
|
deps.append(node.node_tree)
|
||||||
|
elif has_texture(node):
|
||||||
|
deps.append(node.texture)
|
||||||
|
|
||||||
|
return deps
|
@ -1,150 +0,0 @@
|
|||||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
# ##### END GPL LICENSE BLOCK #####
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
from bpy.app.handlers import persistent
|
|
||||||
from replication import porcelain
|
|
||||||
from replication.constants import RP_COMMON, STATE_ACTIVE, STATE_SYNCING, UP
|
|
||||||
from replication.exception import ContextError, NonAuthorizedOperationError
|
|
||||||
from replication.interface import session
|
|
||||||
|
|
||||||
from . import shared_data, utils
|
|
||||||
|
|
||||||
|
|
||||||
def sanitize_deps_graph(remove_nodes: bool = False):
|
|
||||||
""" Cleanup the replication graph
|
|
||||||
"""
|
|
||||||
if session and session.state == STATE_ACTIVE:
|
|
||||||
start = utils.current_milli_time()
|
|
||||||
rm_cpt = 0
|
|
||||||
for node in session.repository.graph.values():
|
|
||||||
node.instance = session.repository.rdp.resolve(node.data)
|
|
||||||
if node is None \
|
|
||||||
or (node.state == UP and not node.instance):
|
|
||||||
if remove_nodes:
|
|
||||||
try:
|
|
||||||
porcelain.rm(session.repository,
|
|
||||||
node.uuid,
|
|
||||||
remove_dependencies=False)
|
|
||||||
logging.info(f"Removing {node.uuid}")
|
|
||||||
rm_cpt += 1
|
|
||||||
except NonAuthorizedOperationError:
|
|
||||||
continue
|
|
||||||
logging.info(f"Sanitize took { utils.current_milli_time()-start} ms, removed {rm_cpt} nodes")
|
|
||||||
|
|
||||||
|
|
||||||
def update_external_dependencies():
|
|
||||||
"""Force external dependencies(files such as images) evaluation
|
|
||||||
"""
|
|
||||||
nodes_ids = [n.uuid for n in session.repository.graph.values() if n.data['type_id'] in ['WindowsPath', 'PosixPath']]
|
|
||||||
for node_id in nodes_ids:
|
|
||||||
node = session.repository.graph.get(node_id)
|
|
||||||
if node and node.owner in [session.repository.username, RP_COMMON]:
|
|
||||||
porcelain.commit(session.repository, node_id)
|
|
||||||
porcelain.push(session.repository, 'origin', node_id)
|
|
||||||
|
|
||||||
|
|
||||||
@persistent
|
|
||||||
def on_scene_update(scene):
|
|
||||||
"""Forward blender depsgraph update to replication
|
|
||||||
"""
|
|
||||||
if session and session.state == STATE_ACTIVE:
|
|
||||||
context = bpy.context
|
|
||||||
blender_depsgraph = bpy.context.view_layer.depsgraph
|
|
||||||
dependency_updates = [u for u in blender_depsgraph.updates]
|
|
||||||
settings = utils.get_preferences()
|
|
||||||
incoming_updates = shared_data.session.applied_updates
|
|
||||||
|
|
||||||
distant_update = [getattr(u.id, 'uuid', None) for u in dependency_updates if getattr(u.id, 'uuid', None) in incoming_updates]
|
|
||||||
if distant_update:
|
|
||||||
for u in distant_update:
|
|
||||||
shared_data.session.applied_updates.remove(u)
|
|
||||||
logging.debug(f"Ignoring distant update of {dependency_updates[0].id.name}")
|
|
||||||
return
|
|
||||||
|
|
||||||
update_external_dependencies()
|
|
||||||
|
|
||||||
# NOTE: maybe we don't need to check each update but only the first
|
|
||||||
for update in reversed(dependency_updates):
|
|
||||||
update_uuid = getattr(update.id, 'uuid', None)
|
|
||||||
if update_uuid:
|
|
||||||
node = session.repository.graph.get(update.id.uuid)
|
|
||||||
check_common = session.repository.rdp.get_implementation(update.id).bl_check_common
|
|
||||||
|
|
||||||
if node and (node.owner == session.repository.username or check_common):
|
|
||||||
logging.debug(f"Evaluate {update.id.name}")
|
|
||||||
if node.state == UP:
|
|
||||||
try:
|
|
||||||
porcelain.commit(session.repository, node.uuid)
|
|
||||||
porcelain.push(session.repository,
|
|
||||||
'origin', node.uuid)
|
|
||||||
except ReferenceError:
|
|
||||||
logging.debug(f"Reference error {node.uuid}")
|
|
||||||
except ContextError as e:
|
|
||||||
logging.debug(e)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(e)
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
elif isinstance(update.id, bpy.types.Scene):
|
|
||||||
scn_uuid = porcelain.add(session.repository, update.id)
|
|
||||||
porcelain.commit(session.repository, scn_uuid)
|
|
||||||
porcelain.push(session.repository, 'origin', scn_uuid)
|
|
||||||
|
|
||||||
|
|
||||||
@persistent
|
|
||||||
def resolve_deps_graph(dummy):
|
|
||||||
"""Resolve deps graph
|
|
||||||
|
|
||||||
Temporary solution to resolve each node pointers after a Undo.
|
|
||||||
A future solution should be to avoid storing dataclock reference...
|
|
||||||
|
|
||||||
"""
|
|
||||||
if session and session.state == STATE_ACTIVE:
|
|
||||||
sanitize_deps_graph(remove_nodes=True)
|
|
||||||
|
|
||||||
|
|
||||||
@persistent
|
|
||||||
def load_pre_handler(dummy):
|
|
||||||
if session and session.state in [STATE_ACTIVE, STATE_SYNCING]:
|
|
||||||
bpy.ops.session.stop()
|
|
||||||
|
|
||||||
|
|
||||||
@persistent
|
|
||||||
def update_client_frame(scene):
|
|
||||||
if session and session.state == STATE_ACTIVE:
|
|
||||||
porcelain.update_user_metadata(session.repository, {
|
|
||||||
'frame_current': scene.frame_current
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
|
||||||
bpy.app.handlers.undo_post.append(resolve_deps_graph)
|
|
||||||
bpy.app.handlers.redo_post.append(resolve_deps_graph)
|
|
||||||
|
|
||||||
bpy.app.handlers.load_pre.append(load_pre_handler)
|
|
||||||
bpy.app.handlers.frame_change_pre.append(update_client_frame)
|
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
|
||||||
bpy.app.handlers.undo_post.remove(resolve_deps_graph)
|
|
||||||
bpy.app.handlers.redo_post.remove(resolve_deps_graph)
|
|
||||||
|
|
||||||
bpy.app.handlers.load_pre.remove(load_pre_handler)
|
|
||||||
bpy.app.handlers.frame_change_pre.remove(update_client_frame)
|
|
Submodule multi_user/libs/replication updated: ce3a7b4363...cb4cdd0444
@ -27,12 +27,12 @@ import shutil
|
|||||||
import string
|
import string
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import traceback
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
|
import traceback
|
||||||
|
|
||||||
from bpy.props import FloatProperty
|
from bpy.props import FloatProperty
|
||||||
|
|
||||||
@ -45,17 +45,16 @@ import bpy
|
|||||||
import mathutils
|
import mathutils
|
||||||
from bpy.app.handlers import persistent
|
from bpy.app.handlers import persistent
|
||||||
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||||
from replication import porcelain
|
|
||||||
from replication.constants import (COMMITED, FETCHED, RP_COMMON, STATE_ACTIVE,
|
from replication.constants import (COMMITED, FETCHED, RP_COMMON, STATE_ACTIVE,
|
||||||
STATE_INITIAL, STATE_SYNCING, UP)
|
STATE_INITIAL, STATE_SYNCING, UP)
|
||||||
|
from replication.protocol import DataTranslationProtocol
|
||||||
from replication.exception import ContextError, NonAuthorizedOperationError
|
from replication.exception import ContextError, NonAuthorizedOperationError
|
||||||
from replication.interface import session
|
from replication.interface import session
|
||||||
from replication.objects import Node
|
from replication import porcelain
|
||||||
from replication.protocol import DataTranslationProtocol
|
|
||||||
from replication.repository import Repository
|
from replication.repository import Repository
|
||||||
|
from replication.objects import Node
|
||||||
|
|
||||||
from . import bl_types, environment, shared_data, timers, ui, utils
|
from . import bl_types, environment, timers, ui, utils
|
||||||
from .handlers import on_scene_update, sanitize_deps_graph
|
|
||||||
from .presence import SessionStatusWidget, renderer, view3d_find
|
from .presence import SessionStatusWidget, renderer, view3d_find
|
||||||
from .timers import registry
|
from .timers import registry
|
||||||
|
|
||||||
@ -100,7 +99,7 @@ def initialize_session():
|
|||||||
|
|
||||||
# Step 2: Load nodes
|
# Step 2: Load nodes
|
||||||
logging.info("Applying nodes")
|
logging.info("Applying nodes")
|
||||||
for node in session.repository.heads:
|
for node in session.repository.index_sorted:
|
||||||
porcelain.apply(session.repository, node)
|
porcelain.apply(session.repository, node)
|
||||||
|
|
||||||
logging.info("Registering timers")
|
logging.info("Registering timers")
|
||||||
@ -113,7 +112,7 @@ def initialize_session():
|
|||||||
utils.flush_history()
|
utils.flush_history()
|
||||||
|
|
||||||
# Step 6: Launch deps graph update handling
|
# Step 6: Launch deps graph update handling
|
||||||
bpy.app.handlers.depsgraph_update_post.append(on_scene_update)
|
bpy.app.handlers.depsgraph_update_post.append(depsgraph_evaluation)
|
||||||
|
|
||||||
|
|
||||||
@session_callback('on_exit')
|
@session_callback('on_exit')
|
||||||
@ -133,8 +132,8 @@ def on_connection_end(reason="none"):
|
|||||||
|
|
||||||
stop_modal_executor = True
|
stop_modal_executor = True
|
||||||
|
|
||||||
if on_scene_update in bpy.app.handlers.depsgraph_update_post:
|
if depsgraph_evaluation in bpy.app.handlers.depsgraph_update_post:
|
||||||
bpy.app.handlers.depsgraph_update_post.remove(on_scene_update)
|
bpy.app.handlers.depsgraph_update_post.remove(depsgraph_evaluation)
|
||||||
|
|
||||||
# Step 3: remove file handled
|
# Step 3: remove file handled
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
@ -604,9 +603,9 @@ class SessionApply(bpy.types.Operator):
|
|||||||
node_ref = session.repository.graph.get(self.target)
|
node_ref = session.repository.graph.get(self.target)
|
||||||
porcelain.apply(session.repository,
|
porcelain.apply(session.repository,
|
||||||
self.target,
|
self.target,
|
||||||
force=True)
|
force=True,
|
||||||
|
force_dependencies=self.reset_dependencies)
|
||||||
impl = session.repository.rdp.get_implementation(node_ref.instance)
|
impl = session.repository.rdp.get_implementation(node_ref.instance)
|
||||||
# NOTE: find another way to handle child and parent automatic reloading
|
|
||||||
if impl.bl_reload_parent:
|
if impl.bl_reload_parent:
|
||||||
for parent in session.repository.graph.get_parents(self.target):
|
for parent in session.repository.graph.get_parents(self.target):
|
||||||
logging.debug(f"Refresh parent {parent}")
|
logging.debug(f"Refresh parent {parent}")
|
||||||
@ -614,11 +613,6 @@ class SessionApply(bpy.types.Operator):
|
|||||||
porcelain.apply(session.repository,
|
porcelain.apply(session.repository,
|
||||||
parent.uuid,
|
parent.uuid,
|
||||||
force=True)
|
force=True)
|
||||||
if hasattr(impl, 'bl_reload_child') and impl.bl_reload_child:
|
|
||||||
for dep in node_ref.dependencies:
|
|
||||||
porcelain.apply(session.repository,
|
|
||||||
dep,
|
|
||||||
force=True)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.report({'ERROR'}, repr(e))
|
self.report({'ERROR'}, repr(e))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@ -642,7 +636,7 @@ class SessionCommit(bpy.types.Operator):
|
|||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
try:
|
try:
|
||||||
porcelain.commit(session.repository, self.target)
|
porcelain.commit(session.repository, self.target)
|
||||||
porcelain.push(session.repository, 'origin', self.target, force=True)
|
porcelain.push(session.repository, 'origin', self.target)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.report({'ERROR'}, repr(e))
|
self.report({'ERROR'}, repr(e))
|
||||||
@ -690,7 +684,6 @@ class SessionPurgeOperator(bpy.types.Operator):
|
|||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
try:
|
try:
|
||||||
sanitize_deps_graph(remove_nodes=True)
|
sanitize_deps_graph(remove_nodes=True)
|
||||||
porcelain.purge_orphan_nodes(session.repository)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.report({'ERROR'}, repr(e))
|
self.report({'ERROR'}, repr(e))
|
||||||
|
|
||||||
@ -723,6 +716,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)
|
||||||
|
|
||||||
@ -925,6 +919,110 @@ classes = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_external_dependencies():
|
||||||
|
nodes_ids = [n.uuid for n in session.repository.graph.values() if n.data['type_id'] in ['WindowsPath', 'PosixPath']]
|
||||||
|
for node_id in nodes_ids:
|
||||||
|
node = session.repository.graph.get(node_id)
|
||||||
|
if node and node.owner in [session.repository.username, RP_COMMON]:
|
||||||
|
porcelain.commit(session.repository, node_id)
|
||||||
|
porcelain.push(session.repository,'origin', node_id)
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_deps_graph(remove_nodes: bool = False):
|
||||||
|
""" Cleanup the replication graph
|
||||||
|
"""
|
||||||
|
if session and session.state == STATE_ACTIVE:
|
||||||
|
start = utils.current_milli_time()
|
||||||
|
rm_cpt = 0
|
||||||
|
for node in session.repository.graph.values():
|
||||||
|
node.instance = session.repository.rdp.resolve(node.data)
|
||||||
|
if node is None \
|
||||||
|
or (node.state == UP and not node.instance):
|
||||||
|
if remove_nodes:
|
||||||
|
try:
|
||||||
|
porcelain.rm(session.repository,
|
||||||
|
node.uuid,
|
||||||
|
remove_dependencies=False)
|
||||||
|
logging.info(f"Removing {node.uuid}")
|
||||||
|
rm_cpt += 1
|
||||||
|
except NonAuthorizedOperationError:
|
||||||
|
continue
|
||||||
|
logging.info(f"Sanitize took { utils.current_milli_time()-start} ms, removed {rm_cpt} nodes")
|
||||||
|
|
||||||
|
|
||||||
|
@persistent
|
||||||
|
def resolve_deps_graph(dummy):
|
||||||
|
"""Resolve deps graph
|
||||||
|
|
||||||
|
Temporary solution to resolve each node pointers after a Undo.
|
||||||
|
A future solution should be to avoid storing dataclock reference...
|
||||||
|
|
||||||
|
"""
|
||||||
|
if session and session.state == STATE_ACTIVE:
|
||||||
|
sanitize_deps_graph(remove_nodes=True)
|
||||||
|
|
||||||
|
|
||||||
|
@persistent
|
||||||
|
def load_pre_handler(dummy):
|
||||||
|
if session and session.state in [STATE_ACTIVE, STATE_SYNCING]:
|
||||||
|
bpy.ops.session.stop()
|
||||||
|
|
||||||
|
|
||||||
|
@persistent
|
||||||
|
def update_client_frame(scene):
|
||||||
|
if session and session.state == STATE_ACTIVE:
|
||||||
|
porcelain.update_user_metadata(session.repository, {
|
||||||
|
'frame_current': scene.frame_current
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@persistent
|
||||||
|
def depsgraph_evaluation(scene):
|
||||||
|
if session and session.state == STATE_ACTIVE:
|
||||||
|
context = bpy.context
|
||||||
|
blender_depsgraph = bpy.context.view_layer.depsgraph
|
||||||
|
dependency_updates = [u for u in blender_depsgraph.updates]
|
||||||
|
settings = utils.get_preferences()
|
||||||
|
|
||||||
|
update_external_dependencies()
|
||||||
|
|
||||||
|
is_internal = [u for u in dependency_updates if u.is_updated_geometry or u.is_updated_shading or u.is_updated_transform]
|
||||||
|
|
||||||
|
# NOTE: maybe we don't need to check each update but only the first
|
||||||
|
if not is_internal:
|
||||||
|
return
|
||||||
|
for update in reversed(dependency_updates):
|
||||||
|
# Is the object tracked ?
|
||||||
|
if update.id.uuid:
|
||||||
|
# Retrieve local version
|
||||||
|
node = session.repository.graph.get(update.id.uuid)
|
||||||
|
check_common = session.repository.rdp.get_implementation(update.id).bl_check_common
|
||||||
|
# Check our right on this update:
|
||||||
|
# - if its ours or ( under common and diff), launch the
|
||||||
|
# update process
|
||||||
|
# - if its to someone else, ignore the update
|
||||||
|
if node and (node.owner == session.repository.username or check_common):
|
||||||
|
if node.state == UP:
|
||||||
|
try:
|
||||||
|
porcelain.commit(session.repository, node.uuid)
|
||||||
|
porcelain.push(session.repository, 'origin', node.uuid)
|
||||||
|
except ReferenceError:
|
||||||
|
logging.debug(f"Reference error {node.uuid}")
|
||||||
|
except ContextError as e:
|
||||||
|
logging.debug(e)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(e)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
# A new scene is created
|
||||||
|
elif isinstance(update.id, bpy.types.Scene):
|
||||||
|
ref = session.repository.get_node_by_datablock(update.id)
|
||||||
|
if ref:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
scn_uuid = porcelain.add(session.repository, update.id)
|
||||||
|
porcelain.commit(session.node_id, scn_uuid)
|
||||||
|
porcelain.push(session.repository,'origin', scn_uuid)
|
||||||
def register():
|
def register():
|
||||||
from bpy.utils import register_class
|
from bpy.utils import register_class
|
||||||
|
|
||||||
@ -932,6 +1030,13 @@ def register():
|
|||||||
register_class(cls)
|
register_class(cls)
|
||||||
|
|
||||||
|
|
||||||
|
bpy.app.handlers.undo_post.append(resolve_deps_graph)
|
||||||
|
bpy.app.handlers.redo_post.append(resolve_deps_graph)
|
||||||
|
|
||||||
|
bpy.app.handlers.load_pre.append(load_pre_handler)
|
||||||
|
bpy.app.handlers.frame_change_pre.append(update_client_frame)
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
if session and session.state == STATE_ACTIVE:
|
if session and session.state == STATE_ACTIVE:
|
||||||
session.disconnect()
|
session.disconnect()
|
||||||
@ -939,3 +1044,9 @@ def unregister():
|
|||||||
from bpy.utils import unregister_class
|
from bpy.utils import unregister_class
|
||||||
for cls in reversed(classes):
|
for cls in reversed(classes):
|
||||||
unregister_class(cls)
|
unregister_class(cls)
|
||||||
|
|
||||||
|
bpy.app.handlers.undo_post.remove(resolve_deps_graph)
|
||||||
|
bpy.app.handlers.redo_post.remove(resolve_deps_graph)
|
||||||
|
|
||||||
|
bpy.app.handlers.load_pre.remove(load_pre_handler)
|
||||||
|
bpy.app.handlers.frame_change_pre.remove(update_client_frame)
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
# ##### END GPL LICENSE BLOCK #####
|
|
||||||
|
|
||||||
from replication.constants import STATE_INITIAL
|
|
||||||
|
|
||||||
|
|
||||||
class SessionData():
|
|
||||||
""" A structure to share easily the current session data across the addon
|
|
||||||
modules.
|
|
||||||
This object will completely replace the Singleton lying in replication
|
|
||||||
interface module.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.repository = None # The current repository
|
|
||||||
self.remote = None # The active remote
|
|
||||||
self.server = None
|
|
||||||
self.applied_updates = []
|
|
||||||
|
|
||||||
@property
|
|
||||||
def state(self):
|
|
||||||
if self.remote is None:
|
|
||||||
return STATE_INITIAL
|
|
||||||
else:
|
|
||||||
return self.remote.connection_status
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
self.remote = None
|
|
||||||
self.repository = None
|
|
||||||
self.server = None
|
|
||||||
self.applied_updates = []
|
|
||||||
|
|
||||||
|
|
||||||
session = SessionData()
|
|
@ -31,8 +31,6 @@ from .presence import (UserFrustumWidget, UserNameWidget, UserSelectionWidget,
|
|||||||
generate_user_camera, get_view_matrix, refresh_3d_view,
|
generate_user_camera, get_view_matrix, refresh_3d_view,
|
||||||
refresh_sidebar_view, renderer)
|
refresh_sidebar_view, renderer)
|
||||||
|
|
||||||
from . import shared_data
|
|
||||||
|
|
||||||
this = sys.modules[__name__]
|
this = sys.modules[__name__]
|
||||||
|
|
||||||
# Registered timers
|
# Registered timers
|
||||||
@ -116,7 +114,6 @@ class ApplyTimer(Timer):
|
|||||||
|
|
||||||
if node_ref.state == FETCHED:
|
if node_ref.state == FETCHED:
|
||||||
try:
|
try:
|
||||||
shared_data.session.applied_updates.append(node)
|
|
||||||
porcelain.apply(session.repository, node)
|
porcelain.apply(session.repository, node)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Fail to apply {node_ref.uuid}")
|
logging.error(f"Fail to apply {node_ref.uuid}")
|
||||||
@ -129,11 +126,6 @@ class ApplyTimer(Timer):
|
|||||||
porcelain.apply(session.repository,
|
porcelain.apply(session.repository,
|
||||||
parent.uuid,
|
parent.uuid,
|
||||||
force=True)
|
force=True)
|
||||||
if hasattr(impl, 'bl_reload_child') and impl.bl_reload_child:
|
|
||||||
for dep in node_ref.dependencies:
|
|
||||||
porcelain.apply(session.repository,
|
|
||||||
dep,
|
|
||||||
force=True)
|
|
||||||
|
|
||||||
|
|
||||||
class DynamicRightSelectTimer(Timer):
|
class DynamicRightSelectTimer(Timer):
|
||||||
@ -259,7 +251,6 @@ class DynamicRightSelectTimer(Timer):
|
|||||||
is_selectable = not session.repository.is_node_readonly(object_uuid)
|
is_selectable = not session.repository.is_node_readonly(object_uuid)
|
||||||
if obj.hide_select != is_selectable:
|
if obj.hide_select != is_selectable:
|
||||||
obj.hide_select = is_selectable
|
obj.hide_select = is_selectable
|
||||||
shared_data.session.applied_updates.append(object_uuid)
|
|
||||||
|
|
||||||
|
|
||||||
class ClientUpdate(Timer):
|
class ClientUpdate(Timer):
|
||||||
|
@ -38,14 +38,6 @@ from replication.constants import (STATE_ACTIVE, STATE_AUTH,
|
|||||||
STATE_LOBBY,
|
STATE_LOBBY,
|
||||||
CONNECTING)
|
CONNECTING)
|
||||||
|
|
||||||
CLEARED_DATABLOCKS = ['actions', 'armatures', 'cache_files', 'cameras',
|
|
||||||
'collections', 'curves', 'filepath', 'fonts',
|
|
||||||
'grease_pencils', 'images', 'lattices', 'libraries',
|
|
||||||
'lightprobes', 'lights', 'linestyles', 'masks',
|
|
||||||
'materials', 'meshes', 'metaballs', 'movieclips',
|
|
||||||
'node_groups', 'objects', 'paint_curves', 'particles',
|
|
||||||
'scenes', 'shape_keys', 'sounds', 'speakers', 'texts',
|
|
||||||
'textures', 'volumes', 'worlds']
|
|
||||||
|
|
||||||
def find_from_attr(attr_name, attr_value, list):
|
def find_from_attr(attr_name, attr_value, list):
|
||||||
for item in list:
|
for item in list:
|
||||||
@ -109,25 +101,23 @@ def get_state_str(state):
|
|||||||
|
|
||||||
|
|
||||||
def clean_scene():
|
def clean_scene():
|
||||||
for type_name in CLEARED_DATABLOCKS:
|
to_delete = [f for f in dir(bpy.data) if f not in ['brushes', 'palettes']]
|
||||||
sub_collection_to_avoid = [
|
for type_name in to_delete:
|
||||||
bpy.data.linestyles.get('LineStyle'),
|
try:
|
||||||
bpy.data.materials.get('Dots Stroke')
|
sub_collection_to_avoid = [bpy.data.linestyles['LineStyle'], bpy.data.materials['Dots Stroke']]
|
||||||
]
|
|
||||||
|
|
||||||
type_collection = getattr(bpy.data, type_name)
|
type_collection = getattr(bpy.data, type_name)
|
||||||
items_to_remove = [i for i in type_collection if i not in sub_collection_to_avoid]
|
items_to_remove = [i for i in type_collection if i not in sub_collection_to_avoid]
|
||||||
for item in items_to_remove:
|
for item in items_to_remove:
|
||||||
try:
|
try:
|
||||||
type_collection.remove(item)
|
type_collection.remove(item)
|
||||||
logging.info(item.name)
|
except:
|
||||||
|
continue
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Clear sequencer
|
# Clear sequencer
|
||||||
bpy.context.scene.sequence_editor_clear()
|
bpy.context.scene.sequence_editor_clear()
|
||||||
|
|
||||||
|
|
||||||
def get_selected_objects(scene, active_view_layer):
|
def get_selected_objects(scene, active_view_layer):
|
||||||
return [obj.uuid for obj in scene.objects if obj.select_get(view_layer=active_view_layer)]
|
return [obj.uuid for obj in scene.objects if obj.select_get(view_layer=active_view_layer)]
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ from multi_user.bl_types.bl_material import BlMaterial
|
|||||||
|
|
||||||
|
|
||||||
def test_material_nodes(clear_blend):
|
def test_material_nodes(clear_blend):
|
||||||
nodes_types = [node.bl_rna.identifier for node in bpy.types.ShaderNode.__subclasses__()]
|
nodes_types = [node.bl_rna.identifier for node in bpy.types.ShaderNode.__subclasses__()] # Faire un peu comme ici
|
||||||
|
|
||||||
datablock = bpy.data.materials.new("test")
|
datablock = bpy.data.materials.new("test")
|
||||||
datablock.use_nodes = True
|
datablock.use_nodes = True
|
||||||
|
@ -11,7 +11,8 @@ from multi_user.utils import get_preferences
|
|||||||
def test_scene(clear_blend):
|
def test_scene(clear_blend):
|
||||||
get_preferences().sync_flags.sync_render_settings = True
|
get_preferences().sync_flags.sync_render_settings = True
|
||||||
|
|
||||||
datablock = bpy.data.scenes.new("toto")
|
# datablock = bpy.data.scenes.new("toto") # TODO: trouver datablock -> active compositing 'Use nodes'
|
||||||
|
datablock = bpy.data.scenes["Scene"].use_nodes
|
||||||
datablock.view_settings.use_curve_mapping = True
|
datablock.view_settings.use_curve_mapping = True
|
||||||
# Test
|
# Test
|
||||||
implementation = BlScene()
|
implementation = BlScene()
|
||||||
|
Reference in New Issue
Block a user