multi-user/operators.py
2019-04-29 16:39:11 +02:00

652 lines
18 KiB
Python

import logging
import random
import string
import time
import asyncio
import queue
from operator import itemgetter
import subprocess
import bgl
import blf
import bpy
import os
import gpu
import mathutils
from bpy_extras import view3d_utils
from gpu_extras.batch import batch_for_shader
from . import client, ui, draw, helpers
from .libs import umsgpack
logger = logging.getLogger(__name__)
client_instance = None
client_keys = None
client_state = 1
server = None
context = None
drawer = None
update_list = {}
SUPPORTED_DATABLOCKS = ['collections', 'meshes', 'objects',
'materials', 'textures', 'lights', 'cameras', 'actions', 'armatures', 'grease_pencils']
SUPPORTED_TYPES = ['Material',
'Texture', 'Light', 'Camera', 'Mesh', 'Grease Pencil', 'Object', 'Action', 'Armature', 'Collection', 'Scene']
# UTILITY FUNCTIONS
def client_list_callback(scene, context):
global client_keys
items = []
if client_keys:
for k in client_keys:
if 'Client' in k[0]:
name = k[1]
items.append((name, name, ""))
return items
def clean_scene(elements=SUPPORTED_DATABLOCKS):
for datablock in elements:
datablock_ref = getattr(bpy.data, datablock)
for item in datablock_ref:
datablock_ref.remove(item)
def randomStringDigits(stringLength=6):
"""Generate a random string of letters and digits """
lettersAndDigits = string.ascii_letters + string.digits
return ''.join(random.choice(lettersAndDigits) for i in range(stringLength))
def randomColor():
r = random.random()
v = random.random()
b = random.random()
return [r, v, b]
def refresh_window():
import bpy
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
def upload_client_instance_position():
global client_instance
username = bpy.context.scene.session_settings.username
if client_instance:
key = "Client/{}".format(username)
try:
current_coords = draw.get_client_view_rect()
client = client_instance.get(key)
if current_coords != client[0][1]['location']:
client[0][1]['location'] = current_coords
client_instance.set(key, client[0][1])
except:
pass
def update_selected_object(context):
global client_instance
session = bpy.context.scene.session_settings
username = bpy.context.scene.session_settings.username
client_key = "Client/{}".format(username)
client_data = client_instance.get(client_key)
selected_objects = helpers.get_selected_objects(context.scene)
if len(selected_objects) > 0:
for obj in selected_objects:
if obj not in client_data[0][1]['active_objects']:
client_data[0][1]['active_objects'] = selected_objects
client_instance.set(client_key,client_data[0][1])
break
elif client_data and client_data[0][1]['active_objects']:
client_data[0][1]['active_objects'] = []
client_instance.set(client_key,client_data[0][1])
# for update in local_updates:
# client_instance.get('')
# if session.active_object is not context.selected_objects[0] or session.active_object.is_evaluated:
# session.active_object = context.selected_objects[0]
# key = "net/objects/{}".format(client_instance.id.decode())
# data = {}
# data['color'] = [session.client_instance_color.r,
# session.client_instance_color.g, session.client_instance_color.b]
# data['object'] = session.active_object.name
# client_instance.push_update(
# key, 'client_instanceObject', data)
# return True
# elif len(context.selected_objects) == 0 and session.active_object:
# session.active_object = None
# data = {}
# data['color'] = [session.client_instance_color.r,
# session.client_instance_color.g, session.client_instance_color.b]
# data['object'] = None
# key = "net/objects/{}".format(client_instance.id.decode())
# client_instance.push_update(key, 'client_instanceObject', data)
# return True
# return False
def update_rights():
C = bpy.context
D = bpy.data
client = C.scene.session_settings.username
for obj in C.scene.objects:
if obj.id == client:
D.objects[obj.name].hide_select = False
else:
D.objects[obj.name].hide_select = True
def init_datablocks():
global client_instance
for datatype in SUPPORTED_TYPES:
for item in getattr(bpy.data, helpers.CORRESPONDANCE[datatype]):
item.id= bpy.context.scene.session_settings.username
key = "{}/{}".format(datatype, item.name)
client_instance.add(key)
def default_tick():
bpy.ops.session.refresh()
# global client_instance
# if not client_instance.queue.empty():
# update = client_instance.queue.get()
# helpers.load(update[0],update[1])
return 0.5
def draw_tick():
# drawing
global drawer
drawer.draw()
# refresh_window()
# Upload
upload_client_instance_position()
update_rights()
return .2
def sync():
global client_instance
if client_instance:
for datatype in SUPPORTED_TYPES:
for item in getattr(bpy.data, helpers.CORRESPONDANCE[datatype]):
if item.id == 'None':
item.id= bpy.context.scene.session_settings.username
key = "{}/{}".format(datatype, item.name)
client_instance.add(key)
return .2
def register_ticks():
# REGISTER Updaters
bpy.app.timers.register(draw_tick)
bpy.app.timers.register(sync)
bpy.app.timers.register(default_tick)
def unregister_ticks():
# REGISTER Updaters
global drawer
drawer.unregister_handlers()
bpy.app.timers.unregister(draw_tick)
bpy.app.timers.unregister(default_tick)
# OPERATORS
class session_join(bpy.types.Operator):
bl_idname = "session.join"
bl_label = "join"
bl_description = "connect to a net server"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return True
def execute(self, context):
global client_instance, drawer
net_settings = context.scene.session_settings
# Scene setup
if net_settings.session_mode == "CONNECT" and net_settings.clear_scene:
clean_scene()
# Session setup
if net_settings.username == "DefaultUser":
net_settings.username = "{}_{}".format(
net_settings.username, randomStringDigits())
username = str(context.scene.session_settings.username)
if len(net_settings.ip) < 1:
net_settings.ip = "127.0.0.1"
client_instance = client.RCFClient()
client_instance.connect(net_settings.username,
net_settings.ip, net_settings.port)
# net_settings.is_running = True
drawer = draw.HUD(client_instance=client_instance)
register_ticks()
return {"FINISHED"}
class session_refresh(bpy.types.Operator):
bl_idname = "session.refresh"
bl_label = "refresh"
bl_description = "refresh client ui keys "
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return True
def execute(self, context):
global client_instance, client_keys,client_state
client_keys = client_instance.list()
client_state = client_instance.state()
return {"FINISHED"}
class session_add_property(bpy.types.Operator):
bl_idname = "session.add_prop"
bl_label = "add"
bl_description = "broadcast a property to connected client_instances"
bl_options = {"REGISTER"}
property_path: bpy.props.StringProperty(default="None")
depth: bpy.props.IntProperty(default=1)
@classmethod
def poll(cls, context):
return True
def execute(self, context):
global client_instance
client_instance.add(self.property_path)
return {"FINISHED"}
class session_get_property(bpy.types.Operator):
bl_idname = "session.get_prop"
bl_label = "get"
bl_description = "broadcast a property to connected client_instances"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return True
def execute(self, context):
global client_instance
client_instance.get("client")
return {"FINISHED"}
class session_remove_property(bpy.types.Operator):
bl_idname = "session.remove_prop"
bl_label = "remove"
bl_description = "broadcast a property to connected client_instances"
bl_options = {"REGISTER"}
property_path: bpy.props.StringProperty(default="None")
@classmethod
def poll(cls, context):
return True
def execute(self, context):
global client_instance
try:
del client_instance.property_map[self.property_path]
return {"FINISHED"}
except:
return {"CANCELED"}
class session_create(bpy.types.Operator):
bl_idname = "session.create"
bl_label = "create"
bl_description = "create to a net session"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return True
def execute(self, context):
global server
global client_instance
net_settings = context.scene.session_settings
server = subprocess.Popen(
['python', 'server.py'], shell=False, stdout=subprocess.PIPE)
# time.sleep(0.1)
bpy.ops.session.join()
if net_settings.init_scene:
init_datablocks()
net_settings.is_admin = True
return {"FINISHED"}
class session_stop(bpy.types.Operator):
bl_idname = "session.stop"
bl_label = "close"
bl_description = "stop net service"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return True
def execute(self, context):
global server
global client_instance, client_keys, client_state
net_settings = context.scene.session_settings
if server:
server.kill()
time.sleep(0.25)
server = None
if client_instance:
client_instance.exit()
time.sleep(0.25)
del client_instance
client_instance = None
client_keys = None
net_settings.is_admin = False
client_state = 1
unregister_ticks()
else:
logger.debug("No server/client_instance running.")
return {"FINISHED"}
class session_rights(bpy.types.Operator):
bl_idname = "session.right"
bl_label = "close"
bl_description = "stop net service"
bl_options = {"REGISTER"}
key: bpy.props.StringProperty(default="None")
@classmethod
def poll(cls, context):
return True
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
net_settings = context.scene.session_settings
col = layout.column()
col.prop(net_settings,"clients")
def execute(self, context):
global server
global client_instance, client_keys, client_state
net_settings = context.scene.session_settings
if net_settings.is_admin:
val = client_instance.get(self.key)
val[0][1]['id'] = net_settings.clients
client_instance.set(key=self.key, value=val[0][1],override=True)
print("Updating {} rights to {}".format(self.key,net_settings.clients))
else:
print("Not admin")
return {"FINISHED"}
class session_settings(bpy.types.PropertyGroup):
username = bpy.props.StringProperty(
name="Username", default="user_{}".format(randomStringDigits()))
ip = bpy.props.StringProperty(
name="ip", description='Distant host ip', default="127.0.0.1")
port = bpy.props.IntProperty(
name="port", description='Distant host port', default=5555)
add_property_depth = bpy.props.IntProperty(
name="add_property_depth", default=1)
buffer = bpy.props.StringProperty(name="None")
is_admin = bpy.props.BoolProperty(name="is_admin", default=False)
load_data = bpy.props.BoolProperty(name="load_data", default=True)
init_scene = bpy.props.BoolProperty(name="load_data", default=True)
clear_scene = bpy.props.BoolProperty(name="clear_scene", default=True)
update_frequency = bpy.props.FloatProperty(
name="update_frequency", default=0.008)
active_object = bpy.props.PointerProperty(
name="active_object", type=bpy.types.Object)
session_mode = bpy.props.EnumProperty(
name='session_mode',
description='session mode',
items={
('HOST', 'hosting', 'host a session'),
('CONNECT', 'connexion', 'connect to a session')},
default='HOST')
client_color = bpy.props.FloatVectorProperty(name="client_instance_color",
subtype='COLOR',
default=randomColor())
clients = bpy.props.EnumProperty(
name="clients",
description="client enum",
items=client_list_callback
)
class session_snapview(bpy.types.Operator):
bl_idname = "session.snapview"
bl_label = "draw client_instances"
bl_description = "Description that shows in blender tooltips"
bl_options = {"REGISTER"}
target_client = bpy.props.StringProperty()
@classmethod
def poll(cls, context):
return True
def execute(self, context):
global client_instance
area, region, rv3d = draw.view3d_find()
client = client_instance.get("Client/{}".format(self.target_client))
if client:
rv3d.view_location = client[0][1]['location'][0]
rv3d.view_distance = 30.0
return {"FINISHED"}
return {"CANCELLED"}
pass
# TODO: Rename to match official blender convention
classes = (
session_join,
session_refresh,
session_add_property,
session_get_property,
session_stop,
session_create,
session_settings,
session_remove_property,
session_snapview,
session_rights,
)
def ordered(updates):
# sorted = sorted(updates, key=lambda tup: SUPPORTED_TYPES.index(tup[1].id.bl_rna.name))
uplist = []
for item in updates.items():
if item[1].id.bl_rna.name in SUPPORTED_TYPES:
uplist.append((SUPPORTED_TYPES.index(
item[1].id.bl_rna.name), item[1].id.bl_rna.name, item[1].id.name,item[1].id ))
uplist.sort(key=itemgetter(0))
return uplist
def exist(update):
global client_keys
key = "{}/{}".format(update.id.bl_rna.name, update.id.name)
if key in client_keys:
return True
else:
return False
def depsgraph_update(scene):
global client_instance
global client_keys
if client_instance and client_instance.agent.is_alive():
updates = bpy.context.depsgraph.updates
username = bpy.context.scene.session_settings.username
update_selected_object(bpy.context)
# selected_objects = helpers.get_selected_objects(scene)
# if len(selected_objects) > 0:
# for update in updates:
# update_key = "{}/{}".format(update.id.bl_rna.name, update.id.name)
# remote_update = client_instance.get(update_key)
# if remote_update and remote_update[0][1]["uuid"] == update.id.uuid:
# print("{} sending update".format(username))
# client_instance.set(
# "{}/{}".format(update.id.bl_rna.name, update.id.name))
# else:
# print("{} applying update".format(username))
# for update in ordered(updates):
# if update[2] == "Master Collection":
# pass
# elif update[1] in SUPPORTED_TYPES:
# key = "{}/{}".format(update[1], update[2])
# data = client_instance.get(key)
# if data:
# if update[3].id == username:
# # Queue update
# client_instance.set(key)
# else:
# # Instance new object ?
# print("new")
# client_instance.add(key)
selected_objects = helpers.get_selected_objects(scene)
if len(selected_objects) > 0:
for updated_data in updates:
if updated_data.id.name in selected_objects:
if updated_data.is_updated_transform or updated_data.is_updated_geometry:
client_instance.set(
"{}/{}".format(updated_data.id.bl_rna.name, updated_data.id.name))
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
bpy.types.ID.id = bpy.props.StringProperty(default="None")
bpy.types.Scene.session_settings = bpy.props.PointerProperty(
type=session_settings)
bpy.app.handlers.depsgraph_update_post.append(depsgraph_update)
def unregister():
global server
global client_instance, client_keys
try:
bpy.app.handlers.depsgraph_update_post.remove(depsgraph_update)
except:
pass
if server:
server.kill()
server = None
del server
if client_instance:
client_instance.exit()
client_instance = None
del client_instance
del client_keys
from bpy.utils import unregister_class
for cls in reversed(classes):
unregister_class(cls)
del bpy.types.Scene.session_settings
del bpy.types.ID.id
if __name__ == "__main__":
register()