Compare commits
16 Commits
v0.1.1
...
132-fix-un
Author | SHA1 | Date | |
---|---|---|---|
45437660ba | |||
ee93a5b209 | |||
f90c12b27f | |||
3573db0969 | |||
92bde00a5a | |||
2c82560d24 | |||
6f364d2b88 | |||
760b52c02b | |||
4dd932fc56 | |||
ba1a03cbfa | |||
18b5fa795c | |||
1a82ec72e4 | |||
804747c73b | |||
7ee705332f | |||
4bd0055056 | |||
f151c61d7b |
Binary file not shown.
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 12 KiB |
BIN
docs/getting_started/img/quickstart_status.png
Normal file
BIN
docs/getting_started/img/quickstart_status.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 70 KiB |
@ -251,6 +251,14 @@ it draw users related information in your viewport such as:
|
|||||||
The presence overlay panel (see image above) allow you to enable/disable
|
The presence overlay panel (see image above) allow you to enable/disable
|
||||||
various drawn parts via the following flags:
|
various drawn parts via the following flags:
|
||||||
|
|
||||||
|
- **Show session statut**: display the session status in the viewport
|
||||||
|
|
||||||
|
.. figure:: img/quickstart_status.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
- **Text scale**: session status text size
|
||||||
|
- **Vertical/Horizontal position**: session position in the viewport
|
||||||
|
|
||||||
- **Show selected objects**: display other users current selection
|
- **Show selected objects**: display other users current selection
|
||||||
- **Show users**: display users current viewpoint
|
- **Show users**: display users current viewpoint
|
||||||
- **Show different scenes**: display users working on other scenes
|
- **Show different scenes**: display users working on other scenes
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Multi-User",
|
"name": "Multi-User",
|
||||||
"author": "Swann Martinez",
|
"author": "Swann Martinez",
|
||||||
"version": (0, 1, 1),
|
"version": (0, 2, 0),
|
||||||
"description": "Enable real-time collaborative workflow inside blender",
|
"description": "Enable real-time collaborative workflow inside blender",
|
||||||
"blender": (2, 82, 0),
|
"blender": (2, 82, 0),
|
||||||
"location": "3D View > Sidebar > Multi-User tab",
|
"location": "3D View > Sidebar > Multi-User tab",
|
||||||
@ -44,7 +44,7 @@ from . import environment
|
|||||||
|
|
||||||
|
|
||||||
DEPENDENCIES = {
|
DEPENDENCIES = {
|
||||||
("replication", '0.1.3'),
|
("replication", '0.2.0'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -92,7 +92,6 @@ def load_driver(target_datablock, src_driver):
|
|||||||
def get_datablock_from_uuid(uuid, default, ignore=[]):
|
def get_datablock_from_uuid(uuid, default, ignore=[]):
|
||||||
if not uuid:
|
if not uuid:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
for category in dir(bpy.data):
|
for category in dir(bpy.data):
|
||||||
root = getattr(bpy.data, category)
|
root = getattr(bpy.data, category)
|
||||||
if isinstance(root, Iterable) and category not in ignore:
|
if isinstance(root, Iterable) and category not in ignore:
|
||||||
@ -123,14 +122,14 @@ class BlDatablock(ReplicatedDatablock):
|
|||||||
# TODO: use is_library_indirect
|
# TODO: use is_library_indirect
|
||||||
self.is_library = (instance and hasattr(instance, 'library') and
|
self.is_library = (instance and hasattr(instance, 'library') and
|
||||||
instance.library) or \
|
instance.library) or \
|
||||||
(self.data and 'library' in self.data)
|
(hasattr(self,'data') and self.data and 'library' in self.data)
|
||||||
|
|
||||||
if instance and hasattr(instance, 'uuid'):
|
if instance and hasattr(instance, 'uuid'):
|
||||||
instance.uuid = self.uuid
|
instance.uuid = self.uuid
|
||||||
|
|
||||||
self.diff_method = DIFF_BINARY
|
# self.diff_method = DIFF_BINARY
|
||||||
|
|
||||||
def resolve(self):
|
def resolve(self, construct = True):
|
||||||
datablock_ref = None
|
datablock_ref = None
|
||||||
datablock_root = getattr(bpy.data, self.bl_id)
|
datablock_root = getattr(bpy.data, self.bl_id)
|
||||||
datablock_ref = utils.find_from_attr('uuid', self.uuid, datablock_root)
|
datablock_ref = utils.find_from_attr('uuid', self.uuid, datablock_root)
|
||||||
@ -139,14 +138,18 @@ class BlDatablock(ReplicatedDatablock):
|
|||||||
try:
|
try:
|
||||||
datablock_ref = datablock_root[self.data['name']]
|
datablock_ref = datablock_root[self.data['name']]
|
||||||
except Exception:
|
except Exception:
|
||||||
name = self.data.get('name')
|
if construct:
|
||||||
logging.debug(f"Constructing {name}")
|
name = self.data.get('name')
|
||||||
datablock_ref = self._construct(data=self.data)
|
logging.debug(f"Constructing {name}")
|
||||||
|
datablock_ref = self._construct(data=self.data)
|
||||||
|
|
||||||
if datablock_ref:
|
if datablock_ref is not None:
|
||||||
setattr(datablock_ref, 'uuid', self.uuid)
|
setattr(datablock_ref, 'uuid', self.uuid)
|
||||||
|
self.instance = datablock_ref
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
self.instance = datablock_ref
|
|
||||||
|
|
||||||
def remove_instance(self):
|
def remove_instance(self):
|
||||||
"""
|
"""
|
||||||
|
@ -259,15 +259,7 @@ class BlMaterial(BlDatablock):
|
|||||||
]
|
]
|
||||||
data = mat_dumper.dump(instance)
|
data = mat_dumper.dump(instance)
|
||||||
|
|
||||||
if instance.use_nodes:
|
if instance.is_grease_pencil:
|
||||||
nodes = {}
|
|
||||||
data["node_tree"] = {}
|
|
||||||
for node in instance.node_tree.nodes:
|
|
||||||
nodes[node.name] = dump_node(node)
|
|
||||||
data["node_tree"]['nodes'] = nodes
|
|
||||||
|
|
||||||
data["node_tree"]["links"] = dump_links(instance.node_tree.links)
|
|
||||||
elif instance.is_grease_pencil:
|
|
||||||
gp_mat_dumper = Dumper()
|
gp_mat_dumper = Dumper()
|
||||||
gp_mat_dumper.depth = 3
|
gp_mat_dumper.depth = 3
|
||||||
|
|
||||||
@ -299,6 +291,14 @@ class BlMaterial(BlDatablock):
|
|||||||
# 'fill_image',
|
# 'fill_image',
|
||||||
]
|
]
|
||||||
data['grease_pencil'] = gp_mat_dumper.dump(instance.grease_pencil)
|
data['grease_pencil'] = gp_mat_dumper.dump(instance.grease_pencil)
|
||||||
|
elif instance.use_nodes:
|
||||||
|
nodes = {}
|
||||||
|
data["node_tree"] = {}
|
||||||
|
for node in instance.node_tree.nodes:
|
||||||
|
nodes[node.name] = dump_node(node)
|
||||||
|
data["node_tree"]['nodes'] = nodes
|
||||||
|
|
||||||
|
data["node_tree"]["links"] = dump_links(instance.node_tree.links)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _resolve_deps_implementation(self):
|
def _resolve_deps_implementation(self):
|
||||||
|
@ -25,7 +25,7 @@ import numpy as np
|
|||||||
from .dump_anything import Dumper, Loader, np_load_collection_primitives, np_dump_collection_primitive, np_load_collection, np_dump_collection
|
from .dump_anything import Dumper, Loader, np_load_collection_primitives, np_dump_collection_primitive, np_load_collection, np_dump_collection
|
||||||
from replication.constants import DIFF_BINARY
|
from replication.constants import DIFF_BINARY
|
||||||
from replication.exception import ContextError
|
from replication.exception import ContextError
|
||||||
from .bl_datablock import BlDatablock
|
from .bl_datablock import BlDatablock, get_datablock_from_uuid
|
||||||
|
|
||||||
VERTICE = ['co']
|
VERTICE = ['co']
|
||||||
|
|
||||||
@ -70,8 +70,17 @@ class BlMesh(BlDatablock):
|
|||||||
# MATERIAL SLOTS
|
# MATERIAL SLOTS
|
||||||
target.materials.clear()
|
target.materials.clear()
|
||||||
|
|
||||||
for m in data["material_list"]:
|
for mat_uuid, mat_name in data["material_list"]:
|
||||||
target.materials.append(bpy.data.materials[m])
|
mat_ref = None
|
||||||
|
if mat_uuid is not None:
|
||||||
|
mat_ref = get_datablock_from_uuid(mat_uuid, None)
|
||||||
|
else:
|
||||||
|
mat_ref = bpy.data.materials.get(mat_name, None)
|
||||||
|
|
||||||
|
if mat_ref is None:
|
||||||
|
raise Exception("Material doesn't exist")
|
||||||
|
|
||||||
|
target.materials.append(mat_ref)
|
||||||
|
|
||||||
# CLEAR GEOMETRY
|
# CLEAR GEOMETRY
|
||||||
if target.vertices:
|
if target.vertices:
|
||||||
@ -166,7 +175,7 @@ class BlMesh(BlDatablock):
|
|||||||
m_list = []
|
m_list = []
|
||||||
for material in instance.materials:
|
for material in instance.materials:
|
||||||
if material:
|
if material:
|
||||||
m_list.append(material.name)
|
m_list.append((material.uuid,material.name))
|
||||||
|
|
||||||
data['material_list'] = m_list
|
data['material_list'] = m_list
|
||||||
|
|
||||||
|
@ -171,7 +171,8 @@ class DynamicRightSelectTimer(Timer):
|
|||||||
session.change_owner(
|
session.change_owner(
|
||||||
node.uuid,
|
node.uuid,
|
||||||
RP_COMMON,
|
RP_COMMON,
|
||||||
recursive=recursive)
|
ignore_warnings=True,
|
||||||
|
affect_dependencies=recursive)
|
||||||
except NonAuthorizedOperationError:
|
except NonAuthorizedOperationError:
|
||||||
logging.warning(f"Not authorized to change {node} owner")
|
logging.warning(f"Not authorized to change {node} owner")
|
||||||
|
|
||||||
@ -188,7 +189,8 @@ class DynamicRightSelectTimer(Timer):
|
|||||||
session.change_owner(
|
session.change_owner(
|
||||||
node.uuid,
|
node.uuid,
|
||||||
settings.username,
|
settings.username,
|
||||||
recursive=recursive)
|
ignore_warnings=True,
|
||||||
|
affect_dependencies=recursive)
|
||||||
except NonAuthorizedOperationError:
|
except NonAuthorizedOperationError:
|
||||||
logging.warning(f"Not authorized to change {node} owner")
|
logging.warning(f"Not authorized to change {node} owner")
|
||||||
else:
|
else:
|
||||||
@ -213,7 +215,8 @@ class DynamicRightSelectTimer(Timer):
|
|||||||
session.change_owner(
|
session.change_owner(
|
||||||
key,
|
key,
|
||||||
RP_COMMON,
|
RP_COMMON,
|
||||||
recursive=recursive)
|
ignore_warnings=True,
|
||||||
|
affect_dependencies=recursive)
|
||||||
except NonAuthorizedOperationError:
|
except NonAuthorizedOperationError:
|
||||||
logging.warning(f"Not authorized to change {key} owner")
|
logging.warning(f"Not authorized to change {key} owner")
|
||||||
|
|
||||||
|
@ -226,7 +226,8 @@ class SessionStartOperator(bpy.types.Operator):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.report({'ERROR'}, repr(e))
|
self.report({'ERROR'}, repr(e))
|
||||||
logging.error(f"Error: {e}")
|
logging.error(f"Error: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
# Join a session
|
# Join a session
|
||||||
else:
|
else:
|
||||||
if not runtime_settings.admin:
|
if not runtime_settings.admin:
|
||||||
@ -426,7 +427,8 @@ class SessionPropertyRightOperator(bpy.types.Operator):
|
|||||||
if session:
|
if session:
|
||||||
session.change_owner(self.key,
|
session.change_owner(self.key,
|
||||||
runtime_settings.clients,
|
runtime_settings.clients,
|
||||||
recursive=self.recursive)
|
ignore_warnings=True,
|
||||||
|
affect_dependencies=self.recursive)
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@ -695,10 +697,12 @@ def sanitize_deps_graph(dummy):
|
|||||||
A future solution should be to avoid storing dataclock reference...
|
A future solution should be to avoid storing dataclock reference...
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if session and session.state['STATE'] == STATE_ACTIVE:
|
if session and session.state['STATE'] == STATE_ACTIVE:
|
||||||
for node_key in session.list():
|
for node_key in session.list():
|
||||||
session.get(node_key).resolve()
|
node = session.get(node_key)
|
||||||
|
if node and not node.resolve(construct=False):
|
||||||
|
session.remove(node_key)
|
||||||
|
|
||||||
@persistent
|
@persistent
|
||||||
def load_pre_handler(dummy):
|
def load_pre_handler(dummy):
|
||||||
|
@ -238,6 +238,31 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
|||||||
set=set_log_level,
|
set=set_log_level,
|
||||||
get=get_log_level
|
get=get_log_level
|
||||||
)
|
)
|
||||||
|
presence_hud_scale: bpy.props.FloatProperty(
|
||||||
|
name="Text scale",
|
||||||
|
description="Adjust the session widget text scale",
|
||||||
|
min=7,
|
||||||
|
max=90,
|
||||||
|
default=15,
|
||||||
|
)
|
||||||
|
presence_hud_hpos: bpy.props.FloatProperty(
|
||||||
|
name="Horizontal position",
|
||||||
|
description="Adjust the session widget horizontal position",
|
||||||
|
min=1,
|
||||||
|
max=90,
|
||||||
|
default=3,
|
||||||
|
step=1,
|
||||||
|
subtype='PERCENTAGE',
|
||||||
|
)
|
||||||
|
presence_hud_vpos: bpy.props.FloatProperty(
|
||||||
|
name="Vertical position",
|
||||||
|
description="Adjust the session widget vertical position",
|
||||||
|
min=1,
|
||||||
|
max=94,
|
||||||
|
default=1,
|
||||||
|
step=1,
|
||||||
|
subtype='PERCENTAGE',
|
||||||
|
)
|
||||||
conf_session_identity_expanded: bpy.props.BoolProperty(
|
conf_session_identity_expanded: bpy.props.BoolProperty(
|
||||||
name="Identity",
|
name="Identity",
|
||||||
description="Identity",
|
description="Identity",
|
||||||
@ -412,6 +437,15 @@ class SessionPrefs(bpy.types.AddonPreferences):
|
|||||||
emboss=False)
|
emboss=False)
|
||||||
if self.conf_session_ui_expanded:
|
if self.conf_session_ui_expanded:
|
||||||
box.row().prop(self, "panel_category", text="Panel category", expand=True)
|
box.row().prop(self, "panel_category", text="Panel category", expand=True)
|
||||||
|
row = box.row()
|
||||||
|
row.label(text="Session widget:")
|
||||||
|
|
||||||
|
col = box.column(align=True)
|
||||||
|
col.prop(self, "presence_hud_scale", expand=True)
|
||||||
|
|
||||||
|
|
||||||
|
col.prop(self, "presence_hud_hpos", expand=True)
|
||||||
|
col.prop(self, "presence_hud_vpos", expand=True)
|
||||||
|
|
||||||
if self.category == 'UPDATE':
|
if self.category == 'UPDATE':
|
||||||
from . import addon_updater_ops
|
from . import addon_updater_ops
|
||||||
|
@ -35,7 +35,7 @@ from replication.constants import (STATE_ACTIVE, STATE_AUTH, STATE_CONFIG,
|
|||||||
STATE_SYNCING, STATE_WAITING)
|
STATE_SYNCING, STATE_WAITING)
|
||||||
from replication.interface import session
|
from replication.interface import session
|
||||||
|
|
||||||
from .utils import find_from_attr, get_state_str
|
from .utils import find_from_attr, get_state_str, get_preferences
|
||||||
|
|
||||||
# Helper functions
|
# Helper functions
|
||||||
|
|
||||||
@ -301,40 +301,37 @@ class UserSelectionWidget(Widget):
|
|||||||
if not ob:
|
if not ob:
|
||||||
return
|
return
|
||||||
|
|
||||||
position = None
|
vertex_pos = bbox_from_obj(ob, 1.0)
|
||||||
|
vertex_indices = ((0, 1), (0, 2), (1, 3), (2, 3),
|
||||||
|
(4, 5), (4, 6), (5, 7), (6, 7),
|
||||||
|
(0, 4), (1, 5), (2, 6), (3, 7))
|
||||||
|
|
||||||
if ob.type == 'EMPTY':
|
if ob.instance_collection:
|
||||||
# TODO: Child case
|
for obj in ob.instance_collection.objects:
|
||||||
# Collection instance case
|
if obj.type == 'MESH' and hasattr(obj, 'bound_box'):
|
||||||
indices = (
|
vertex_pos = get_bb_coords_from_obj(obj, instance=ob)
|
||||||
(0, 1), (1, 2), (2, 3), (0, 3),
|
break
|
||||||
(4, 5), (5, 6), (6, 7), (4, 7),
|
elif ob.type == 'EMPTY':
|
||||||
(0, 4), (1, 5), (2, 6), (3, 7))
|
vertex_pos = bbox_from_obj(ob, ob.empty_display_size)
|
||||||
if ob.instance_collection:
|
elif ob.type == 'LIGHT':
|
||||||
for obj in ob.instance_collection.objects:
|
vertex_pos = bbox_from_obj(ob, ob.data.shadow_soft_size)
|
||||||
if obj.type == 'MESH' and hasattr(obj, 'bound_box'):
|
elif ob.type == 'LIGHT_PROBE':
|
||||||
positions = get_bb_coords_from_obj(obj, instance=ob)
|
vertex_pos = bbox_from_obj(ob, ob.data.influence_distance)
|
||||||
break
|
elif ob.type == 'CAMERA':
|
||||||
|
vertex_pos = bbox_from_obj(ob, ob.data.display_size)
|
||||||
elif hasattr(ob, 'bound_box'):
|
elif hasattr(ob, 'bound_box'):
|
||||||
indices = (
|
vertex_indices = (
|
||||||
(0, 1), (1, 2), (2, 3), (0, 3),
|
(0, 1), (1, 2), (2, 3), (0, 3),
|
||||||
(4, 5), (5, 6), (6, 7), (4, 7),
|
(4, 5), (5, 6), (6, 7), (4, 7),
|
||||||
(0, 4), (1, 5), (2, 6), (3, 7))
|
(0, 4), (1, 5), (2, 6), (3, 7))
|
||||||
positions = get_bb_coords_from_obj(ob)
|
vertex_pos = get_bb_coords_from_obj(ob)
|
||||||
if positions is None:
|
|
||||||
indices = (
|
|
||||||
(0, 1), (0, 2), (1, 3), (2, 3),
|
|
||||||
(4, 5), (4, 6), (5, 7), (6, 7),
|
|
||||||
(0, 4), (1, 5), (2, 6), (3, 7))
|
|
||||||
|
|
||||||
positions = bbox_from_obj(ob, ob.scale.x)
|
|
||||||
|
|
||||||
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
|
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
|
||||||
batch = batch_for_shader(
|
batch = batch_for_shader(
|
||||||
shader,
|
shader,
|
||||||
'LINES',
|
'LINES',
|
||||||
{"pos": positions},
|
{"pos": vertex_pos},
|
||||||
indices=indices)
|
indices=vertex_indices)
|
||||||
|
|
||||||
shader.bind()
|
shader.bind()
|
||||||
shader.uniform_float("color", self.data.get('color'))
|
shader.uniform_float("color", self.data.get('color'))
|
||||||
@ -387,6 +384,9 @@ class UserNameWidget(Widget):
|
|||||||
class SessionStatusWidget(Widget):
|
class SessionStatusWidget(Widget):
|
||||||
draw_type = 'POST_PIXEL'
|
draw_type = 'POST_PIXEL'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.preferences = get_preferences()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def settings(self):
|
def settings(self):
|
||||||
return getattr(bpy.context.window_manager, 'session', None)
|
return getattr(bpy.context.window_manager, 'session', None)
|
||||||
@ -396,6 +396,8 @@ class SessionStatusWidget(Widget):
|
|||||||
self.settings.enable_presence
|
self.settings.enable_presence
|
||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
|
text_scale = self.preferences.presence_hud_scale
|
||||||
|
ui_scale = bpy.context.preferences.view.ui_scale
|
||||||
color = [1, 1, 0, 1]
|
color = [1, 1, 0, 1]
|
||||||
state = session.state.get('STATE')
|
state = session.state.get('STATE')
|
||||||
state_str = f"{get_state_str(state)}"
|
state_str = f"{get_state_str(state)}"
|
||||||
@ -404,9 +406,11 @@ class SessionStatusWidget(Widget):
|
|||||||
color = [0, 1, 0, 1]
|
color = [0, 1, 0, 1]
|
||||||
elif state == STATE_INITIAL:
|
elif state == STATE_INITIAL:
|
||||||
color = [1, 0, 0, 1]
|
color = [1, 0, 0, 1]
|
||||||
|
hpos = (self.preferences.presence_hud_hpos*bpy.context.area.width)/100
|
||||||
|
vpos = (self.preferences.presence_hud_vpos*bpy.context.area.height)/100
|
||||||
|
|
||||||
blf.position(0, 10, 20, 0)
|
blf.position(0, hpos, vpos, 0)
|
||||||
blf.size(0, 16, 45)
|
blf.size(0, int(text_scale*ui_scale), 72)
|
||||||
blf.color(0, color[0], color[1], color[2], color[3])
|
blf.color(0, color[0], color[1], color[2], color[3])
|
||||||
blf.draw(0, state_str)
|
blf.draw(0, state_str)
|
||||||
|
|
||||||
|
@ -448,9 +448,17 @@ class SESSION_PT_presence(bpy.types.Panel):
|
|||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
|
||||||
settings = context.window_manager.session
|
settings = context.window_manager.session
|
||||||
|
pref = get_preferences()
|
||||||
layout.active = settings.enable_presence
|
layout.active = settings.enable_presence
|
||||||
col = layout.column()
|
col = layout.column()
|
||||||
col.prop(settings, "presence_show_session_status")
|
col.prop(settings, "presence_show_session_status")
|
||||||
|
row = col.column()
|
||||||
|
row.active = settings.presence_show_session_status
|
||||||
|
row.prop(pref, "presence_hud_scale", expand=True)
|
||||||
|
row = col.column(align=True)
|
||||||
|
row.active = settings.presence_show_session_status
|
||||||
|
row.prop(pref, "presence_hud_hpos", expand=True)
|
||||||
|
row.prop(pref, "presence_hud_vpos", expand=True)
|
||||||
col.prop(settings, "presence_show_selected")
|
col.prop(settings, "presence_show_selected")
|
||||||
col.prop(settings, "presence_show_user")
|
col.prop(settings, "presence_show_user")
|
||||||
row = layout.column()
|
row = layout.column()
|
||||||
|
Reference in New Issue
Block a user