import logging import random import string import time import asyncio import queue from operator import itemgetter import subprocess import uuid 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 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 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) # Active object bounding box if len(context.selected_objects) > 0: for obj in context.selected_objects: if obj.name not in client_data[0][1]['active_objects']: client_data[0][1]['active_objects'] = helpers.get_selected_objects(context.scene) client_instance.set(client_key,client_data[0][1]) break elif client_data[0][1]['active_objects']: client_data[0][1]['active_objects'] = [] client_instance.set(client_key,client_data[0][1]) # 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 init_datablocks(): global client_instance for datatype in SUPPORTED_TYPES: for item in getattr(bpy.data, helpers.CORRESPONDANCE[datatype]): key = "{}/{}".format(datatype, item.name) print(key) client_instance.set(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() return .2 def register_ticks(): # REGISTER Updaters bpy.app.timers.register(draw_tick) 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_keys = client_instance.list() 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.set(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 server = subprocess.Popen( ['python', 'server.py'], shell=False, stdout=subprocess.PIPE) time.sleep(0.1) bpy.ops.session.join() if context.scene.session_settings.init_scene: init_datablocks() 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 net_settings = context.scene.session_settings if server: server.kill() del server server = None if client_instance: client_instance.exit() del client_instance client_instance = None del client_keys client_keys = None net_settings.is_running = False unregister_ticks() else: logger.debug("No server/client_instance running.") 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_running = bpy.props.BoolProperty(name="is_running", 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()) 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 = net_draw.view3d_find() for k, v in client_instance.property_map.items(): if v.mtype == 'client_instance' and v.id.decode() == self.target_client_instance: rv3d.view_location = v.body['location'][1] 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, ) 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)) uplist.sort(key=itemgetter(0)) return uplist def is_dirty(updates): global client_keys if client_keys: if len(client_keys) > 0: for u in updates: key = "{}/{}".format(u.id.bl_rna.name, u.id.name) if key not in client_keys: return True return False def depsgraph_update(scene): global client_instance if client_instance and client_instance.agent.is_alive(): updates = bpy.context.depsgraph.updates update_selected_object(bpy.context) if is_dirty(updates): for update in ordered(updates): if update[2] == "Master Collection": pass elif update[1] in SUPPORTED_TYPES: client_instance.set("{}/{}".format(update[1], update[2])) if hasattr(bpy.context, 'selected_objects'): if len(bpy.context.selected_objects) > 0: updated_data = updates[0] if updated_data.id.name == bpy.context.selected_objects[0].name: 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.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) 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.is_dirty if __name__ == "__main__": register()