multi-user/operators.py
2019-05-02 18:38:06 +02:00

563 lines
15 KiB
Python

import asyncio
import logging
import os
import queue
import random
import string
import subprocess
import time
from operator import itemgetter
import bgl
import blf
import bpy
import gpu
import mathutils
from bpy_extras import view3d_utils
from gpu_extras.batch import batch_for_shader
from . import client, draw, helpers, ui
from .libs import umsgpack
logger = logging.getLogger(__name__)
client_instance = None
client_keys = None
client_state = 1
server = None
context = 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])
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.set(key)
def default_tick():
bpy.ops.session.refresh()
upload_client_instance_position()
return 1
def register_ticks():
# REGISTER Updaters
bpy.app.timers.register(default_tick)
pass
def unregister_ticks():
# REGISTER Updaters
bpy.app.timers.unregister(default_tick)
pass
# 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
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
# bpy.ops.session.refresh()
register_ticks()
# Launch drawing module
if net_settings.enable_draw:
draw.renderer.run()
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
keys = client_instance.list()
if keys:
client_keys = keys
state = client_instance.state()
if state:
client_state = 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()
# client_instance.init()
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()
# Stop drawing
draw.renderer.stop()
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
)
enable_draw = bpy.props.BoolProperty(
name="enable_draw", description='Enable overlay drawing module', default=True)
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.net_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 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))
for update in updates:
if update.id.id == username:
getattr(bpy.data, helpers.CORRESPONDANCE[update.id.__class__.__name__])[update.id.name].is_dirty= True
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.ID.is_dirty = bpy.props.BoolProperty(default=False)
bpy.types.Scene.session_settings = bpy.props.PointerProperty(
type=session_settings)
bpy.app.handlers.depsgraph_update_post.append(depsgraph_update)
draw.register()
def unregister():
global server
global client_instance, client_keys
draw.unregister()
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
del bpy.types.ID.is_dirty
if __name__ == "__main__":
register()