import asyncio import logging import os import queue import random import string import subprocess import time from operator import itemgetter import queue import bpy import mathutils from bpy_extras import view3d_utils from gpu_extras.batch import batch_for_shader from pathlib import Path python_path = Path(bpy.app.binary_path_python) cwd_for_subprocesses = python_path.parent 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 = {} history = queue.Queue() # UTILITY FUNCTIONS def client_list_callback(scene, context): global client_keys items = [("Common", "Common", "")] username = bpy.context.window_manager.session_settings.username if client_keys: for k in client_keys: if 'Client' in k[0]: name = k[1] if name == username: name += " (self)" items.append((name, name, "")) return items def clean_scene(elements=helpers.SUPPORTED_TYPES): for datablock in elements: datablock_ref =getattr(bpy.data, helpers.CORRESPONDANCE[datablock]) for item in datablock_ref: try: datablock_ref.remove(item) #Catch last scene remove except RuntimeError: pass 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.window_manager.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_client_selected_object(context): global client_instance session = bpy.context.window_manager.session_settings username = bpy.context.window_manager.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]) # Init configuration def init_datablocks(): global client_instance for datatype in helpers.SUPPORTED_TYPES: for item in getattr(bpy.data, helpers.CORRESPONDANCE[datatype]): item.id = bpy.context.window_manager.session_settings.username key = "{}/{}".format(datatype, item.name) client_instance.set(key) def refresh_session_data(): 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 def default_tick(): refresh_session_data() upload_client_instance_position() return .2 def register_ticks(): # REGISTER Updaters bpy.app.timers.register(default_tick) def unregister_ticks(): # REGISTER Updaters 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 net_settings = context.window_manager.session_settings # Scene setup if net_settings.clear_scene: clean_scene() # Session setup if net_settings.username == "DefaultUser": net_settings.username = "{}_{}".format( net_settings.username, randomStringDigits()) username = str(context.window_manager.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): refresh_session_data() 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.window_manager.session_settings script_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)),"server.py") server = subprocess.Popen( [str(python_path),script_dir],shell=False, stdout=subprocess.PIPE) 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.window_manager.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() 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 = "Change owner to" 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.window_manager.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.window_manager.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 is_replicated(update): # global client_keys # dickt = dict(client_keys) global client_instance object_type = update.id.bl_rna.name object_name = update.id.name #Master collection special cae if update.id.name == 'Master Collection': object_type = 'Scene' object_name = bpy.context.scene.name if 'Light' in update.id.bl_rna.name: object_type = 'Light' key = "{}/{}".format(object_type, object_name) if client_instance.exist(key): return True else: logger.info("{} Not rep".format(key)) return False def get_datablock_from_update(update,context): item_type = update.id.__class__.__name__ item_id = update.id.name datablock_ref = None if item_id == 'Master Collection': datablock_ref= bpy.context.scene elif item_type in helpers.CORRESPONDANCE.keys(): datablock_ref = getattr(bpy.data, helpers.CORRESPONDANCE[update.id.__class__.__name__])[update.id.name] else: if item_id in bpy.data.lights.keys(): datablock_ref = bpy.data.lights[item_id] return datablock_ref def toogle_dirty(context, update): data_ref = get_datablock_from_update(update,context) if data_ref: logger.info(update.id.bl_rna.__class__.__name__) data_ref.is_dirty= True def depsgraph_update(scene): global client_instance global client_keys global client_state ctx = bpy.context if client_state == 3: if ctx.mode in ['OBJECT','PAINT_GPENCIL']: updates = ctx.depsgraph.updates username = ctx.window_manager.session_settings.username selected_objects = helpers.get_selected_objects(scene) for update in reversed(updates): if is_replicated(update): if update.id.id == username or update.id.id == 'Common': toogle_dirty(ctx, update) else: item = get_datablock_from_update(update,ctx) #get parent authority if hasattr(item,"id"): parent_id = ctx.collection.id if ctx.collection.id != 'None' else ctx.scene.id if parent_id == username or parent_id == 'Common': item.id = username item_type = item.__class__.__name__ if 'Light'in item.__class__.__name__: item_type = 'Light' key = "{}/{}".format(item_type , item.name) client_instance.set(key) logger.info("APPEND {}".format(key)) else: try: getattr(bpy.data, helpers.CORRESPONDANCE[update.id.__class__.__name__]).remove(item) except: pass break update_client_selected_object(ctx) 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.WindowManager.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.WindowManager.session_settings del bpy.types.ID.id del bpy.types.ID.is_dirty if __name__ == "__main__": register()