Merge branch '126-draw-refactoring' into 'develop'

Resolve "Draw refactoring"

See merge request slumber/multi-user!55
This commit is contained in:
Swann Martinez 2020-10-06 14:12:13 +00:00
commit 8e7be5afde
9 changed files with 444 additions and 344 deletions

View File

@ -7,4 +7,7 @@ build:
name: multi_user
paths:
- multi_user
only:
refs:
- master
- develop

View File

@ -16,3 +16,7 @@ deploy:
- echo "Pushing to gitlab registry ${VERSION}"
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker push registry.gitlab.com/slumber/multi-user/multi-user-server:${VERSION}
only:
refs:
- master
- develop

View File

@ -19,7 +19,7 @@
bl_info = {
"name": "Multi-User",
"author": "Swann Martinez",
"version": (0, 1, 0),
"version": (0, 1, 1),
"description": "Enable real-time collaborative workflow inside blender",
"blender": (2, 82, 0),
"location": "3D View > Sidebar > Multi-User tab",
@ -40,7 +40,7 @@ import sys
import bpy
from bpy.app.handlers import persistent
from . import environment, utils
from . import environment
DEPENDENCIES = {

View File

@ -19,7 +19,15 @@ import logging
import bpy
from . import presence, utils
from . import utils
from .presence import (renderer,
UserFrustumWidget,
UserNameWidget,
UserSelectionWidget,
refresh_3d_view,
generate_user_camera,
get_view_matrix,
refresh_sidebar_view)
from replication.constants import (FETCHED,
UP,
RP_COMMON,
@ -33,9 +41,11 @@ from replication.constants import (FETCHED,
from replication.interface import session
class Delayable():
"""Delayable task interface
"""
def __init__(self):
self.is_registered = False
@ -63,13 +73,14 @@ class Timer(Delayable):
def register(self):
"""Register the timer into the blender timer system
"""
if not self.is_registered:
bpy.app.timers.register(self.main)
self.is_registered = True
logging.debug(f"Register {self.__class__.__name__}")
else:
logging.debug(f"Timer {self.__class__.__name__} already registered")
logging.debug(
f"Timer {self.__class__.__name__} already registered")
def main(self):
self.execute()
@ -211,59 +222,6 @@ class DynamicRightSelectTimer(Timer):
obj.hide_select = True
class Draw(Delayable):
def __init__(self):
super().__init__()
self._handler = None
def register(self):
if not self.is_registered:
self._handler = bpy.types.SpaceView3D.draw_handler_add(
self.execute, (), 'WINDOW', 'POST_VIEW')
logging.debug(f"Register {self.__class__.__name__}")
else:
logging.debug(f"Drow {self.__class__.__name__} already registered")
def execute(self):
raise NotImplementedError()
def unregister(self):
try:
bpy.types.SpaceView3D.draw_handler_remove(
self._handler, "WINDOW")
except:
pass
class DrawClient(Draw):
def execute(self):
renderer = getattr(presence, 'renderer', None)
prefs = utils.get_preferences()
if session and renderer and session.state['STATE'] == STATE_ACTIVE:
settings = bpy.context.window_manager.session
users = session.online_users
# Update users
for user in users.values():
metadata = user.get('metadata')
color = metadata.get('color')
scene_current = metadata.get('scene_current')
user_showable = scene_current == bpy.context.scene.name or settings.presence_show_far_user
if color and scene_current and user_showable:
if settings.presence_show_selected and 'selected_objects' in metadata.keys():
renderer.draw_client_selection(
user['id'], color, metadata['selected_objects'])
if settings.presence_show_user and 'view_corners' in metadata:
renderer.draw_client_camera(
user['id'], metadata['view_corners'], color)
if not user_showable:
# TODO: remove this when user event drivent update will be
# ready
renderer.flush_selection()
renderer.flush_users()
class ClientUpdate(Timer):
def __init__(self, timout=.1):
super().__init__(timout)
@ -272,7 +230,6 @@ class ClientUpdate(Timer):
def execute(self):
settings = utils.get_preferences()
renderer = getattr(presence, 'renderer', None)
if session and renderer:
if session.state['STATE'] in [STATE_ACTIVE, STATE_LOBBY]:
@ -291,7 +248,7 @@ class ClientUpdate(Timer):
if cached_user_data is None:
self.users_metadata[username] = user_data['metadata']
elif 'view_matrix' in cached_user_data and 'view_matrix' in new_user_data and cached_user_data['view_matrix'] != new_user_data['view_matrix']:
presence.refresh_3d_view()
refresh_3d_view()
self.users_metadata[username] = user_data['metadata']
break
else:
@ -300,13 +257,13 @@ class ClientUpdate(Timer):
local_user_metadata = local_user.get('metadata')
scene_current = bpy.context.scene.name
local_user = session.online_users.get(settings.username)
current_view_corners = presence.get_view_corners()
current_view_corners = generate_user_camera()
# Init client metadata
if not local_user_metadata or 'color' not in local_user_metadata.keys():
metadata = {
'view_corners': presence.get_view_matrix(),
'view_matrix': presence.get_view_matrix(),
'view_corners': get_view_matrix(),
'view_matrix': get_view_matrix(),
'color': (settings.client_color.r,
settings.client_color.g,
settings.client_color.b,
@ -323,7 +280,7 @@ class ClientUpdate(Timer):
session.update_user_metadata(local_user_metadata)
elif 'view_corners' in local_user_metadata and current_view_corners != local_user_metadata['view_corners']:
local_user_metadata['view_corners'] = current_view_corners
local_user_metadata['view_matrix'] = presence.get_view_matrix(
local_user_metadata['view_matrix'] = get_view_matrix(
)
session.update_user_metadata(local_user_metadata)
@ -333,26 +290,27 @@ class SessionStatusUpdate(Timer):
super().__init__(timout)
def execute(self):
presence.refresh_sidebar_view()
refresh_sidebar_view()
class SessionUserSync(Timer):
def __init__(self, timout=1):
super().__init__(timout)
self.settings = utils.get_preferences()
def execute(self):
renderer = getattr(presence, 'renderer', None)
if session and renderer:
# sync online users
session_users = session.online_users
ui_users = bpy.context.window_manager.online_users
for index, user in enumerate(ui_users):
if user.username not in session_users.keys():
if user.username not in session_users.keys() and \
user.username != self.settings.username:
renderer.remove_widget(f"{user.username}_cam")
renderer.remove_widget(f"{user.username}_select")
renderer.remove_widget(f"{user.username}_name")
ui_users.remove(index)
renderer.flush_selection()
renderer.flush_users()
break
for user in session_users:
@ -360,13 +318,20 @@ class SessionUserSync(Timer):
new_key = ui_users.add()
new_key.name = user
new_key.username = user
if user != self.settings.username:
renderer.add_widget(
f"{user}_cam", UserFrustumWidget(user))
renderer.add_widget(
f"{user}_select", UserSelectionWidget(user))
renderer.add_widget(
f"{user}_name", UserNameWidget(user))
class MainThreadExecutor(Timer):
def __init__(self, timout=1, execution_queue=None):
super().__init__(timout)
self.execution_queue = execution_queue
def execute(self):
while not self.execution_queue.empty():
function = self.execution_queue.get()

View File

@ -21,26 +21,25 @@ import logging
import os
import queue
import random
import shutil
import string
import time
from operator import itemgetter
from pathlib import Path
import shutil
from pathlib import Path
from queue import Queue
import bpy
import mathutils
from bpy.app.handlers import persistent
from . import bl_types, delayable, environment, presence, ui, utils
from replication.constants import (FETCHED, STATE_ACTIVE,
STATE_INITIAL,
STATE_SYNCING, RP_COMMON, UP)
from replication.constants import (FETCHED, RP_COMMON, STATE_ACTIVE,
STATE_INITIAL, STATE_SYNCING, UP)
from replication.data import ReplicatedDataFactory
from replication.exception import NonAuthorizedOperationError
from replication.interface import session
from . import bl_types, delayable, environment, ui, utils
from .presence import (SessionStatusWidget, refresh_3d_view, renderer,
view3d_find)
background_execution_queue = Queue()
delayables = []
@ -80,10 +79,6 @@ def initialize_session():
if node_ref.state == FETCHED:
node_ref.apply()
# Step 3: Launch presence overlay
if runtime_settings.enable_presence:
presence.renderer.run()
# Step 4: Register blender timers
for d in delayables:
d.register()
@ -108,9 +103,6 @@ def on_connection_end():
stop_modal_executor = True
# Step 2: Unregister presence renderer
presence.renderer.stop()
if settings.update_method == 'DEPSGRAPH':
bpy.app.handlers.depsgraph_update_post.remove(
depsgraph_evaluation)
@ -256,7 +248,6 @@ class SessionStartOperator(bpy.types.Operator):
# Background client updates service
delayables.append(delayable.ClientUpdate())
delayables.append(delayable.DrawClient())
delayables.append(delayable.DynamicRightSelectTimer())
session_update = delayable.SessionStatusUpdate()
@ -472,7 +463,7 @@ class SessionSnapUserOperator(bpy.types.Operator):
return {'CANCELLED'}
if event.type == 'TIMER':
area, region, rv3d = presence.view3d_find()
area, region, rv3d = view3d_find()
if session:
target_ref = session.online_users.get(self.target_client)
@ -746,6 +737,7 @@ def depsgraph_evaluation(scene):
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)

View File

@ -476,25 +476,26 @@ class SessionProps(bpy.types.PropertyGroup):
name="Presence overlay",
description='Enable overlay drawing module',
default=True,
update=presence.update_presence
)
presence_show_selected: bpy.props.BoolProperty(
name="Show selected objects",
description='Enable selection overlay ',
default=True,
update=presence.update_overlay_settings
)
presence_show_user: bpy.props.BoolProperty(
name="Show users",
description='Enable user overlay ',
default=True,
update=presence.update_overlay_settings
)
presence_show_far_user: bpy.props.BoolProperty(
name="Show users on different scenes",
description="Show user on different scenes",
default=False,
update=presence.update_overlay_settings
)
presence_show_session_status: bpy.props.BoolProperty(
name="Show session status ",
description="Show session status on the viewport",
default=True,
)
filter_owned: bpy.props.BoolProperty(
name="filter_owned",

View File

@ -19,6 +19,7 @@
import copy
import logging
import math
import sys
import traceback
import bgl
@ -28,13 +29,17 @@ import gpu
import mathutils
from bpy_extras import view3d_utils
from gpu_extras.batch import batch_for_shader
from replication.constants import (STATE_ACTIVE, STATE_AUTH, STATE_CONFIG,
STATE_INITIAL, STATE_LAUNCHING_SERVICES,
STATE_LOBBY, STATE_QUITTING, STATE_SRV_SYNC,
STATE_SYNCING, STATE_WAITING)
from replication.interface import session
from . import utils
from .utils import find_from_attr, get_state_str
renderer = None
# Helper functions
def view3d_find():
def view3d_find() -> tuple:
""" Find the first 'VIEW_3D' windows found in areas
:return: tuple(Area, Region, RegionView3D)
@ -56,36 +61,48 @@ def refresh_3d_view():
if area and region and rv3d:
area.tag_redraw()
def refresh_sidebar_view():
""" Refresh the blender sidebar
""" Refresh the blender viewport sidebar
"""
area, region, rv3d = view3d_find()
if area:
area.regions[3].tag_redraw()
def get_target(region, rv3d, coord):
def project_to_viewport(region: bpy.types.Region, rv3d: bpy.types.RegionView3D, coords: list, distance: float = 1.0) -> list:
""" Compute a projection from 2D to 3D viewport coordinate
:param region: target windows region
:type region: bpy.types.Region
:param rv3d: view 3D
:type rv3d: bpy.types.RegionView3D
:param coords: coordinate to project
:type coords: list
:param distance: distance offset into viewport
:type distance: float
:return: list of coordinates [x,y,z]
"""
target = [0, 0, 0]
if coord and region and rv3d:
view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord)
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord)
target = ray_origin + view_vector
return [target.x, target.y, target.z]
def get_target_far(region, rv3d, coord, distance):
target = [0, 0, 0]
if coord and region and rv3d:
view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord)
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord)
if coords and region and rv3d:
view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coords)
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coords)
target = ray_origin + view_vector * distance
return [target.x, target.y, target.z]
def get_default_bbox(obj, radius):
def bbox_from_obj(obj: bpy.types.Object, radius: float) -> list:
""" Generate a bounding box for a given object by using its world matrix
:param obj: target object
:type obj: bpy.types.Object
:param radius: bounding box radius
:type radius: float
:return: list of 8 points [(x,y,z),...]
"""
coords = [
(-radius, -radius, -radius), (+radius, -radius, -radius),
(-radius, +radius, -radius), (+radius, +radius, -radius),
@ -93,264 +110,373 @@ def get_default_bbox(obj, radius):
(-radius, +radius, +radius), (+radius, +radius, +radius)]
base = obj.matrix_world
bbox_corners = [base @ mathutils.Vector(corner) for corner in coords]
bbox_corners = [base @ mathutils.Vector(corner) for corner in coords]
return [(point.x, point.y, point.z)
for point in bbox_corners]
for point in bbox_corners]
def get_view_corners():
def generate_user_camera() -> list:
""" Generate a basic camera represention of the user point of view
:return: list of 7 points
"""
area, region, rv3d = view3d_find()
v1 = [0, 0, 0]
v2 = [0, 0, 0]
v3 = [0, 0, 0]
v4 = [0, 0, 0]
v5 = [0, 0, 0]
v6 = [0, 0, 0]
v7 = [0, 0, 0]
v1 = v2 = v3 = v4 = v5 = v6 = v7 = [0, 0, 0]
if area and region and rv3d:
width = region.width
height = region.height
v1 = get_target(region, rv3d, (0, 0))
v3 = get_target(region, rv3d, (0, height))
v2 = get_target(region, rv3d, (width, height))
v4 = get_target(region, rv3d, (width, 0))
v1 = project_to_viewport(region, rv3d, (0, 0))
v3 = project_to_viewport(region, rv3d, (0, height))
v2 = project_to_viewport(region, rv3d, (width, height))
v4 = project_to_viewport(region, rv3d, (width, 0))
v5 = get_target(region, rv3d, (width/2, height/2))
v5 = project_to_viewport(region, rv3d, (width/2, height/2))
v6 = list(rv3d.view_location)
v7 = get_target_far(region, rv3d, (width/2, height/2), -.8)
v7 = project_to_viewport(
region, rv3d, (width/2, height/2), distance=-.8)
coords = [v1, v2, v3, v4, v5, v6, v7]
return coords
def get_client_2d(coords):
def project_to_screen(coords: list) -> list:
""" Project 3D coordinate to 2D screen coordinates
:param coords: 3D coordinates (x,y,z)
:type coords: list
:return: list of 2D coordinates [x,y]
"""
area, region, rv3d = view3d_find()
if area and region and rv3d:
return view3d_utils.location_3d_to_region_2d(region, rv3d, coords)
else:
return (0, 0)
def get_bb_coords_from_obj(object, parent=None):
def get_bb_coords_from_obj(object: bpy.types.Object, parent: bpy.types.Object = None) -> list:
""" Generate bounding box in world coordinate from object bound box
:param object: target object
:type object: bpy.types.Object
:param parent: optionnal parent
:type parent: bpy.types.Object
:return: list of 8 points [(x,y,z),...]
"""
base = object.matrix_world if parent is None else parent.matrix_world
bbox_corners = [base @ mathutils.Vector(
corner) for corner in object.bound_box]
corner) for corner in object.bound_box]
return [(point.x, point.y, point.z)
for point in bbox_corners]
for point in bbox_corners]
def get_view_matrix():
def get_view_matrix() -> list:
""" Return the 3d viewport view matrix
:return: view matrix as a 4x4 list
"""
area, region, rv3d = view3d_find()
if area and region and rv3d:
if area and region and rv3d:
return [list(v) for v in rv3d.view_matrix]
def update_presence(self, context):
global renderer
if 'renderer' in globals() and hasattr(renderer, 'run'):
if self.enable_presence:
renderer.run()
class Widget(object):
""" Base class to define an interface element
"""
draw_type: str = 'POST_VIEW' # Draw event type
def poll(self) -> bool:
"""Test if the widget can be drawn or not
:return: bool
"""
return True
def draw(self):
"""How to draw the widget
"""
raise NotImplementedError()
class UserFrustumWidget(Widget):
# Camera widget indices
indices = ((1, 3), (2, 1), (3, 0),
(2, 0), (4, 5), (1, 6),
(2, 6), (3, 6), (0, 6))
def __init__(
self,
username):
self.username = username
self.settings = bpy.context.window_manager.session
@property
def data(self):
user = session.online_users.get(self.username)
if user:
return user.get('metadata')
else:
renderer.stop()
return None
def poll(self):
if self.data is None:
return False
def update_overlay_settings(self, context):
global renderer
scene_current = self.data.get('scene_current')
view_corners = self.data.get('view_corners')
if renderer and not self.presence_show_selected:
renderer.flush_selection()
if renderer and not self.presence_show_user:
renderer.flush_users()
return (scene_current == bpy.context.scene.name or
self.settings.presence_show_far_user) and \
view_corners and \
self.settings.presence_show_user and \
self.settings.enable_presence
class DrawFactory(object):
def __init__(self):
self.d3d_items = {}
self.d2d_items = {}
self.draw3d_handle = None
self.draw2d_handle = None
self.draw_event = None
self.coords = None
self.active_object = None
def run(self):
self.register_handlers()
def stop(self):
self.flush_users()
self.flush_selection()
self.unregister_handlers()
refresh_3d_view()
def register_handlers(self):
self.draw3d_handle = bpy.types.SpaceView3D.draw_handler_add(
self.draw3d_callback, (), 'WINDOW', 'POST_VIEW')
self.draw2d_handle = bpy.types.SpaceView3D.draw_handler_add(
self.draw2d_callback, (), 'WINDOW', 'POST_PIXEL')
def unregister_handlers(self):
if self.draw2d_handle:
bpy.types.SpaceView3D.draw_handler_remove(
self.draw2d_handle, "WINDOW")
self.draw2d_handle = None
if self.draw3d_handle:
bpy.types.SpaceView3D.draw_handler_remove(
self.draw3d_handle, "WINDOW")
self.draw3d_handle = None
self.d3d_items.clear()
self.d2d_items.clear()
def flush_selection(self, user=None):
key_to_remove = []
select_key = f"{user}_select" if user else "select"
for k in self.d3d_items.keys():
if select_key in k:
key_to_remove.append(k)
for k in key_to_remove:
del self.d3d_items[k]
def flush_users(self):
key_to_remove = []
for k in self.d3d_items.keys():
if "select" not in k:
key_to_remove.append(k)
for k in key_to_remove:
del self.d3d_items[k]
self.d2d_items.clear()
def draw_client_selection(self, client_id, client_color, client_selection):
local_user = utils.get_preferences().username
if local_user != client_id:
self.flush_selection(client_id)
for select_ob in client_selection:
drawable_key = f"{client_id}_select_{select_ob}"
ob = utils.find_from_attr("uuid", select_ob, bpy.data.objects)
if not ob:
return
if ob.type == 'EMPTY':
# TODO: Child case
# Collection instance case
indices = (
(0, 1), (1, 2), (2, 3), (0, 3),
(4, 5), (5, 6), (6, 7), (4, 7),
(0, 4), (1, 5), (2, 6), (3, 7))
if ob.instance_collection:
for obj in ob.instance_collection.objects:
if obj.type == 'MESH':
self.append_3d_item(
drawable_key,
client_color,
get_bb_coords_from_obj(obj, parent=ob),
indices)
if ob.type in ['MESH','META']:
indices = (
(0, 1), (1, 2), (2, 3), (0, 3),
(4, 5), (5, 6), (6, 7), (4, 7),
(0, 4), (1, 5), (2, 6), (3, 7))
self.append_3d_item(
drawable_key,
client_color,
get_bb_coords_from_obj(ob),
indices)
else:
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))
self.append_3d_item(
drawable_key,
client_color,
get_default_bbox(ob, ob.scale.x),
indices)
def append_3d_item(self,key,color, coords, indices):
def draw(self):
location = self.data.get('view_corners')
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
color = color
positions = [tuple(coord) for coord in location]
if len(positions) != 7:
return
batch = batch_for_shader(
shader, 'LINES', {"pos": coords}, indices=indices)
shader,
'LINES',
{"pos": positions},
indices=self.indices)
self.d3d_items[key] = (shader, batch, color)
def draw_client_camera(self, client_id, client_location, client_color):
if client_location:
local_user = utils.get_preferences().username
if local_user != client_id:
try:
indices = (
(1, 3), (2, 1), (3, 0),
(2, 0), (4, 5), (1, 6),
(2, 6), (3, 6), (0, 6)
)
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
position = [tuple(coord) for coord in client_location]
color = client_color
batch = batch_for_shader(
shader, 'LINES', {"pos": position}, indices=indices)
self.d3d_items[client_id] = (shader, batch, color)
self.d2d_items[client_id] = (position[1], client_id, color)
except Exception as e:
logging.debug(f"Draw client exception: {e} \n {traceback.format_exc()}\n pos:{position},ind:{indices}")
def draw3d_callback(self):
bgl.glLineWidth(2.)
bgl.glEnable(bgl.GL_DEPTH_TEST)
bgl.glEnable(bgl.GL_BLEND)
bgl.glEnable(bgl.GL_LINE_SMOOTH)
shader.bind()
shader.uniform_float("color", self.data.get('color'))
batch.draw(shader)
class UserSelectionWidget(Widget):
def __init__(
self,
username):
self.username = username
self.settings = bpy.context.window_manager.session
@property
def data(self):
user = session.online_users.get(self.username)
if user:
return user.get('metadata')
else:
return None
def poll(self):
if self.data is None:
return False
user_selection = self.data.get('selected_objects')
scene_current = self.data.get('scene_current')
return (scene_current == bpy.context.scene.name or
self.settings.presence_show_far_user) and \
user_selection and \
self.settings.presence_show_selected and \
self.settings.enable_presence
def draw(self):
user_selection = self.data.get('selected_objects')
for select_ob in user_selection:
ob = find_from_attr("uuid", select_ob, bpy.data.objects)
if not ob:
return
if ob.type == 'EMPTY':
# TODO: Child case
# Collection instance case
indices = (
(0, 1), (1, 2), (2, 3), (0, 3),
(4, 5), (5, 6), (6, 7), (4, 7),
(0, 4), (1, 5), (2, 6), (3, 7))
if ob.instance_collection:
for obj in ob.instance_collection.objects:
if obj.type == 'MESH':
positions = get_bb_coords_from_obj(obj, parent=ob)
if hasattr(ob, 'bound_box'):
indices = (
(0, 1), (1, 2), (2, 3), (0, 3),
(4, 5), (5, 6), (6, 7), (4, 7),
(0, 4), (1, 5), (2, 6), (3, 7))
positions = get_bb_coords_from_obj(ob)
else:
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')
batch = batch_for_shader(
shader,
'LINES',
{"pos": positions},
indices=indices)
shader.bind()
shader.uniform_float("color", self.data.get('color'))
batch.draw(shader)
class UserNameWidget(Widget):
draw_type = 'POST_PIXEL'
def __init__(
self,
username):
self.username = username
self.settings = bpy.context.window_manager.session
@property
def data(self):
user = session.online_users.get(self.username)
if user:
return user.get('metadata')
else:
return None
def poll(self):
if self.data is None:
return False
scene_current = self.data.get('scene_current')
view_corners = self.data.get('view_corners')
return (scene_current == bpy.context.scene.name or
self.settings.presence_show_far_user) and \
view_corners and \
self.settings.presence_show_user and \
self.settings.enable_presence
def draw(self):
view_corners = self.data.get('view_corners')
color = self.data.get('color')
position = [tuple(coord) for coord in view_corners]
coords = project_to_screen(position[1])
if coords:
blf.position(0, coords[0], coords[1]+10, 0)
blf.size(0, 16, 72)
blf.color(0, color[0], color[1], color[2], color[3])
blf.draw(0, self.username)
class SessionStatusWidget(Widget):
draw_type = 'POST_PIXEL'
@property
def settings(self):
return getattr(bpy.context.window_manager, 'session', None)
def poll(self):
return self.settings and self.settings.presence_show_session_status and \
self.settings.enable_presence
def draw(self):
color = [1, 1, 0, 1]
state = session.state.get('STATE')
state_str = f"{get_state_str(state)}"
if state == STATE_ACTIVE:
color = [0, 1, 0, 1]
elif state == STATE_INITIAL:
color = [1, 0, 0, 1]
blf.position(0, 10, 20, 0)
blf.size(0, 16, 45)
blf.color(0, color[0], color[1], color[2], color[3])
blf.draw(0, state_str)
class DrawFactory(object):
def __init__(self):
self.post_view_handle = None
self.post_pixel_handle = None
self.widgets = {}
def add_widget(self, name: str, widget: Widget):
self.widgets[name] = widget
def remove_widget(self, name: str):
if name in self.widgets:
del self.widgets[name]
else:
logging.error(f"Widget {name} not existing")
def clear_widgets(self):
self.widgets.clear()
def register_handlers(self):
self.post_view_handle = bpy.types.SpaceView3D.draw_handler_add(
self.post_view_callback,
(),
'WINDOW',
'POST_VIEW')
self.post_pixel_handle = bpy.types.SpaceView3D.draw_handler_add(
self.post_pixel_callback,
(),
'WINDOW',
'POST_PIXEL')
def unregister_handlers(self):
if self.post_pixel_handle:
bpy.types.SpaceView3D.draw_handler_remove(
self.post_pixel_handle,
"WINDOW")
self.post_pixel_handle = None
if self.post_view_handle:
bpy.types.SpaceView3D.draw_handler_remove(
self.post_view_handle,
"WINDOW")
self.post_view_handle = None
def post_view_callback(self):
try:
for shader, batch, color in self.d3d_items.values():
shader.bind()
shader.uniform_float("color", color)
batch.draw(shader)
except Exception:
logging.error("3D Exception")
for widget in self.widgets.values():
if widget.draw_type == 'POST_VIEW' and widget.poll():
widget.draw()
except Exception as e:
logging.error(
f"Post view widget exception: {e} \n {traceback.print_exc()}")
def draw2d_callback(self):
for position, font, color in self.d2d_items.values():
try:
coords = get_client_2d(position)
def post_pixel_callback(self):
try:
for widget in self.widgets.values():
if widget.draw_type == 'POST_PIXEL' and widget.poll():
widget.draw()
except Exception as e:
logging.error(
f"Post pixel widget Exception: {e} \n {traceback.print_exc()}")
if coords:
blf.position(0, coords[0], coords[1]+10, 0)
blf.size(0, 16, 72)
blf.color(0, color[0], color[1], color[2], color[3])
blf.draw(0, font)
except Exception:
logging.error("2D EXCEPTION")
this = sys.modules[__name__]
this.renderer = DrawFactory()
def register():
global renderer
renderer = DrawFactory()
this.renderer.register_handlers()
this.renderer.add_widget("session_status", SessionStatusWidget())
def unregister():
global renderer
renderer.unregister_handlers()
this.renderer.unregister_handlers()
del renderer
this.renderer.clear_widgets()

View File

@ -18,7 +18,7 @@
import bpy
from .utils import get_preferences, get_expanded_icon, get_folder_size
from .utils import get_preferences, get_expanded_icon, get_folder_size, get_state_str
from replication.constants import (ADDED, ERROR, FETCHED,
MODIFIED, RP_COMMON, UP,
STATE_ACTIVE, STATE_AUTH,
@ -60,32 +60,6 @@ def printProgressBar(iteration, total, prefix='', suffix='', decimals=1, length=
return f"{prefix} |{bar}| {iteration}/{total}{suffix}"
def get_state_str(state):
state_str = 'UNKNOWN'
if state == STATE_WAITING:
state_str = 'WARMING UP DATA'
elif state == STATE_SYNCING:
state_str = 'FETCHING'
elif state == STATE_AUTH:
state_str = 'AUTHENTIFICATION'
elif state == STATE_CONFIG:
state_str = 'CONFIGURATION'
elif state == STATE_ACTIVE:
state_str = 'ONLINE'
elif state == STATE_SRV_SYNC:
state_str = 'PUSHING'
elif state == STATE_INITIAL:
state_str = 'INIT'
elif state == STATE_QUITTING:
state_str = 'QUITTING'
elif state == STATE_LAUNCHING_SERVICES:
state_str = 'LAUNCHING SERVICES'
elif state == STATE_LOBBY:
state_str = 'LOBBY'
return state_str
class SESSION_PT_settings(bpy.types.Panel):
"""Settings panel"""
bl_idname = "MULTIUSER_SETTINGS_PT_panel"
@ -476,6 +450,7 @@ class SESSION_PT_presence(bpy.types.Panel):
settings = context.window_manager.session
layout.active = settings.enable_presence
col = layout.column()
col.prop(settings, "presence_show_session_status")
col.prop(settings, "presence_show_selected")
col.prop(settings, "presence_show_user")
row = layout.column()
@ -637,14 +612,15 @@ class VIEW3D_PT_overlay_session(bpy.types.Panel):
display_all = overlay.show_overlays
col = layout.column()
col.active = display_all
row = col.row(align=True)
settings = context.window_manager.session
layout.active = settings.enable_presence
col = layout.column()
col.prop(settings, "presence_show_session_status")
col.prop(settings, "presence_show_selected")
col.prop(settings, "presence_show_user")
row = layout.column()
row.active = settings.presence_show_user
row.prop(settings, "presence_show_far_user")

View File

@ -29,7 +29,14 @@ import math
import bpy
import mathutils
from . import environment, presence
from . import environment
from replication.constants import (STATE_ACTIVE, STATE_AUTH,
STATE_CONFIG, STATE_SYNCING,
STATE_INITIAL, STATE_SRV_SYNC,
STATE_WAITING, STATE_QUITTING,
STATE_LOBBY,
STATE_LAUNCHING_SERVICES)
def find_from_attr(attr_name, attr_value, list):
@ -58,6 +65,32 @@ def get_datablock_users(datablock):
return users
def get_state_str(state):
state_str = 'UNKOWN'
if state == STATE_WAITING:
state_str = 'WARMING UP DATA'
elif state == STATE_SYNCING:
state_str = 'FETCHING'
elif state == STATE_AUTH:
state_str = 'AUTHENTIFICATION'
elif state == STATE_CONFIG:
state_str = 'CONFIGURATION'
elif state == STATE_ACTIVE:
state_str = 'ONLINE'
elif state == STATE_SRV_SYNC:
state_str = 'PUSHING'
elif state == STATE_INITIAL:
state_str = 'OFFLINE'
elif state == STATE_QUITTING:
state_str = 'QUITTING'
elif state == STATE_LAUNCHING_SERVICES:
state_str = 'LAUNCHING SERVICES'
elif state == STATE_LOBBY:
state_str = 'LOBBY'
return state_str
def clean_scene():
for type_name in dir(bpy.data):
try: