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 logger = logging.getLogger(__name__) client_instance = None server = None context = None drawer = None update_list = {} push_tasks = queue.Queue() pull_tasks = queue.Queue() def add_update(type, item): try: if item not in update_list[type]: update_list[type].append(item) except KeyError: update_list[type] = [] def get_update(type): try: update = None if update_list[type]: update = update_list[type].pop() except KeyError: update_list[type] = [] return update SUPPORTED_DATABLOCKS = ['collections', 'meshes', 'objects', 'materials', 'textures', 'lights', 'cameras', 'actions', 'armatures', 'grease_pencils'] SUPPORTED_TYPES = ['Mesh', 'Grease Pencil', 'Material', 'Texture', 'Light', 'Camera', '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 if client_instance: key = "net/client_instances/{}".format(client_instance.id.decode()) try: current_coords = net_draw.get_client_instance_view_rect() data = client_instance.property_map[key].body if data is None: data = {} data['location'] = current_coords color = bpy.context.scene.session_settings.client_instance_color data['color'] = (color.r, color.g, color.b, 1) client_instance.push_update(key, 'client_instance', data) elif current_coords[0] != data['location'][0]: data['location'] = current_coords client_instance.push_update(key, 'client_instance', data) except: pass def update_selected_object(context): global client_instance session = bpy.context.scene.session_settings # Active object bounding box if len(context.selected_objects) > 0: 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_scene(): for gp in bpy.data.grease_pencils: upload_gpencil(gp) for cam in bpy.data.cameras: dump_datablock(cam, 1) for light in bpy.data.lights: dump_datablock(light, 1) for mat in bpy.data.materials: dump_datablock(mat, 7) for mesh in bpy.data.meshes: upload_mesh(mesh) for object in bpy.data.objects: dump_datablock(object, 1) for collection in bpy.data.collections: dump_datablock(collection, 4) for scene in bpy.data.scenes: dump_datablock(scene, 4) def update_scene(msg): global client_instance net_vars = bpy.context.scene.session_settings pull_tasks.put(msg.key) # if net_vars.active_object: # if net_vars.active_object.name in msg.key: # raise ValueError() # if 'net' not in msg.key: # target = resolve_bpy_path(msg.key) # if target: # target.is_updating = True # if msg.mtype == 'Object': # load_object(target=target, data=msg.body, # create=net_vars.load_data) # global drawer # drawer.draw() # elif msg.mtype == 'Mesh': # load_mesh(target=target, data=msg.body, # create=net_vars.load_data) # elif msg.mtype == 'Collection': # load_collection(target=target, data=msg.body, # create=net_vars.load_data) # elif msg.mtype == 'Material': # load_material(target=target, data=msg.body, # create=net_vars.load_data) # elif msg.mtype == 'Grease Pencil': # load_gpencil(target=target, data=msg.body, # create=net_vars.load_data) # elif msg.mtype == 'Scene': # load_scene(target=target, data=msg.body, # create=net_vars.load_data) # elif 'Light' in msg.mtype: # load_light(target=target, data=msg.body, # create=net_vars.load_data) # else: # load_default(target=target, data=msg.body, # create=net_vars.load_data, type=msg.mtype) # else: # if msg.mtype == 'client_instance': # refresh_window() # elif msg.mtype == 'client_instanceObject': # selected_objects = [] # for k, v in client_instance.property_map.items(): # if v.mtype == 'client_instanceObject': # if client_instance.id != v.id: # selected_objects.append(v.body['object']) # for obj in bpy.data.objects: # if obj.name in selected_objects: # obj.hide_select = True # else: # obj.hide_select = False # refresh_window() def push(data_type,id): if data_type == 'Material': upload_material(bpy.data.materials[id]) if data_type == 'Grease Pencil': upload_gpencil(bpy.data.grease_pencils[id]) if data_type == 'Camera': dump_datablock(bpy.data.cameras[id], 1) if data_type == 'Light': dump_datablock(bpy.data.lights[id], 1) if data_type == 'Mesh': upload_mesh(bpy.data.meshes[id]) if data_type == 'Object': dump_datablock(bpy.data.objects[id], 1) if data_type == 'Collection': dump_datablock(bpy.data.collections[id], 4) if data_type == 'Scene': dump_datablock(bpy.data.scenes[id], 4) def pull(keystore): global client_instance net_vars = bpy.context.scene.session_settings body = client_instance.property_map[keystore].body data_type = client_instance.property_map[keystore].mtype target = resolve_bpy_path(keystore) if target: target.is_updating = True if data_type == 'Object': load_object(target=target, data=body, create=net_vars.load_data) global drawer drawer.draw() elif data_type == 'Mesh': load_mesh(target=target, data=body, create=net_vars.load_data) elif data_type == 'Collection': load_collection(target=target, data=body, create=net_vars.load_data) elif data_type == 'Material': load_material(target=target, data=body, create=net_vars.load_data) elif data_type == 'Grease Pencil': load_gpencil(target=target, data=body, create=net_vars.load_data) elif data_type == 'Scene': load_scene(target=target, data=body, create=net_vars.load_data) elif 'Light' in data_type: load_light(target=target, data=body, create=net_vars.load_data) elif data_type == 'Camera': load_default(target=target, data=body, create=net_vars.load_data, type=mtype) elif data_type == 'client_instance': refresh_window() elif data_type == 'client_instanceObject': selected_objects = [] for k, v in client_instance.property_map.items(): if v.mtype == 'client_instanceObject': if client_instance.id != v.id: selected_objects.append(v.body['object']) for obj in bpy.data.objects: if obj.name in selected_objects: obj.hide_select = True else: obj.hide_select = False refresh_window() recv_callbacks = [update_scene] post_init_callbacks = [refresh_window] def default_tick(): # for op in bpy.context.window_manager.operators: # try: # if isinstance(op.uuid,tuple): # op.uuid = str(uuid.uuid4()) # except Exception as e: # print("error on {} {}".format(op.name,e)) if not push_tasks.empty(): update = push_tasks.get() print(update) try: push(update[0],update[1]) except Exception as e: print("push error: {}".format(e)) if not pull_tasks.empty(): try: pull(pull_tasks.get()) except Exception as e: print("pull error: {}".format(e)) return 0.1 def mesh_tick(): mesh = get_update("Mesh") if mesh: upload_mesh(bpy.data.meshes[mesh]) return 2 def object_tick(): obj_name = get_update("Object") global client_instance if obj_name: if "Object/{}".format(obj_name) in client_instance.property_map.keys(): dump_datablock_attibute(bpy.data.objects[obj_name], ['matrix_world']) else: dump_datablock(bpy.data.objects[obj_name], 1) return 0.1 def material_tick(): return 2 def draw_tick(): # drawing global drawer drawer.draw() # Upload upload_client_instance_position() return 0.2 def register_ticks(): # REGISTER Updaters bpy.app.timers.register(draw_tick) bpy.app.timers.register(mesh_tick) bpy.app.timers.register(object_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(mesh_tick) bpy.app.timers.unregister(object_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) client_instance = client.RCFClient() client_instance.connect("127.0.0.1",5555) # net_settings.is_running = True # drawer = net_draw.HUD(client_instance_instance=client_instance) # register_ticks() 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) # print(client_instance.get('key')) # item = resolve_bpy_path(self.property_path) # print(item) # if item: # key = self.property_path # dumper = dump_anything.Dumper() # dumper.type_subset = dumper.match_subset_all # dumper.depth = self.depth # data = dumper.dump(item) # data_type = item.__class__.__name__ # client_instance.push_update(key, data_type, data) 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_scene() 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 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 # bpy.ops.asyncio.stop() 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") port = bpy.props.IntProperty(name="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_instance = 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_add_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 = [(SUPPORTED_TYPES.index(item[1].id.bl_rna.name),item[1].id.bl_rna.name,item[1].id.name) for item in updates.items()] uplist.sort(key=itemgetter(0)) return uplist def depsgraph_update(scene): global client_instance if client_instance and client_instance.status == net_components.RCFStatus.CONNECTED: updates = bpy.context.depsgraph.updates update_selected_object(bpy.context) push = True # Update selected object for update in updates.items(): updated_data = update[1] if updated_data.id.is_updating: updated_data.id.is_updating = False push = False break if push: # if len(updates) is 1: # updated_data = updates[0] # if scene.session_settings.active_object and updated_data.id.name == scene.session_settings.active_object.name: # if updated_data.is_updated_transform: # add_update(updated_data.id.bl_rna.name, updated_data.id.name) # else: for update in ordered(updates): if update[2] == "Master Collection": pass elif update[1] in SUPPORTED_TYPES: push_tasks.put((update[1], update[2])) # elif scene.session_settings.active_object and updated_data.id.name == scene.session_settings.active_object.name: # if updated_data.is_updated_transform or updated_data.is_updated_geometry: # add_update(updated_data.id.bl_rna.name, updated_data.id.name) # elif updated_data.id.bl_rna.name in [SUPPORTED_TYPES]: # push_tasks.put((updated_data.id.bl_rna.name, updated_data.id.name)) # for c in reversed(updates.items()): # if c[1].is_updated_geometry: # print("{} - {}".format(c[1].id.name,c[1].id.bl_rna.name)) # for c in updates.items(): # if scene.session_settings.active_object: # if c[1].id.name == scene.session_settings.active_object.name: # if c[1].is_updated_geometry: # add_update(c[1].id.bl_rna.name, c[1].id.name) # elif c[1].is_updated_transform: # add_update(c[1].id.bl_rna.name, c[1].id.name) # else: # pass # print('other{}'.format(c[1].id.name)) # if c[1].id.bl_rna.name == 'Material' or c[1].id.bl_rna.name== 'Shader Nodetree': # print(len(bpy.context.depsgraph.updates.items())) # data_name = c[1].id.name # if c[1].id.bl_rna.name == "Object": # if data_name in bpy.data.objects.keys(): # found = False # for k in client_instance.property_map.keys(): # if data_name in k: # found = True # break # if not found: # pass # upload_mesh(bpy.data.objects[data_name].data) # dump_datablock(bpy.data.objects[data_name], 1) # dump_datablock(bpy.data.scenes[0], 4) # dump_datablock(bpy.data.scenes[0],4) def register(): from bpy.utils import register_class for cls in classes: register_class(cls) bpy.types.ID.is_updating = 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 # try: # bpy.app.handlers.depsgraph_update_post.remove(depsgraph_update) # except: # pass if server: # server.stop() del server server = None if client_instance: # client_instance.stop() del client_instance client_instance = None 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_updating if __name__ == "__main__": register()