import asyncio import logging import os import queue import random import string import subprocess import time from operator import itemgetter from pathlib import Path import bpy import mathutils from bpy_extras.io_utils import ExportHelper from bpy.app.handlers import persistent from . import environment, presence, ui, utils, delayable import msgpack from .libs.replication.replication.data import ReplicatedDataFactory from .libs.replication.replication.interface import Session from .libs.replication.replication.exception import NonAuthorizedOperationError from . import bl_types logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) client = None delayables = [] ui_context = None def add_datablock(datablock): global client new_uuid = client.add(datablock) return new_uuid # TODO: cleanup def init_supported_datablocks(supported_types_id): global client for type_id in supported_types_id: if hasattr(bpy.data, type_id): for item in getattr(bpy.data, type_id): if client.exist(uuid=item.uuid): continue else: client.add(item) # OPERATORS class SessionStartOperator(bpy.types.Operator): bl_idname = "session.start" bl_label = "start" bl_description = "connect to a net server" bl_options = {"REGISTER"} host: bpy.props.BoolProperty(default=False) @classmethod def poll(cls, context): return True def execute(self, context): global client, delayables settings = context.window_manager.session # save config settings.save(context) bpy_factory = ReplicatedDataFactory() supported_bl_types = [] # init the factory with supported types for type in bl_types.types_to_register(): _type = getattr(bl_types, type) supported_bl_types.append(_type.bl_id) # Retreive local replicated types settings type_local_config = settings.supported_datablock[_type.bl_rep_class.__name__] bpy_factory.register_type( _type.bl_class, _type.bl_rep_class, timer=type_local_config.bl_delay_refresh, automatic=type_local_config.auto_push) if type_local_config.bl_delay_apply > 0: delayables.append(delayable.ApplyTimer( timout=type_local_config.bl_delay_apply, target_type=_type.bl_rep_class)) client = Session(factory=bpy_factory) if self.host: # Scene setup if settings.start_empty: utils.clean_scene() client.host( id=settings.username, address=settings.ip, port=settings.port, right_strategy=settings.right_strategy ) settings.is_admin = True else: utils.clean_scene() client.connect( id=settings.username, address=settings.ip, port=settings.port ) if client.state == 0: settings.is_admin = False self.report( {'ERROR'}, "A session is already hosted on this address") return {"CANCELLED"} if settings.init_scene and self.host: init_supported_datablocks(supported_bl_types) for node in client.list(): client.commit(node) # Init user settings usr = presence.User( username=settings.username, color=(settings.client_color.r, settings.client_color.g, settings.client_color.b, 1), ) settings.user_uuid = client.add(usr) client.commit(settings.user_uuid) delayables.append(delayable.ClientUpdate( client_uuid=settings.user_uuid)) delayables.append(delayable.DynamicRightSelectTimer()) # Push all added values client.push() # Launch drawing module if settings.enable_presence: presence.renderer.run() # Register blender main thread tools for d in delayables: d.register() self.report( {'INFO'}, "connexion on tcp://{}:{}".format(settings.ip, settings.port)) return {"FINISHED"} class SessionStopOperator(bpy.types.Operator): bl_idname = "session.stop" bl_label = "close" bl_description = "Exit current session" bl_options = {"REGISTER"} @classmethod def poll(cls, context): return True def execute(self, context): global client, delayables settings = context.window_manager.session settings.is_admin = False assert(client) client.remove(settings.user_uuid) client.disconnect() for d in delayables: try: d.unregister() except: continue presence.renderer.stop() return {"FINISHED"} class SessionPropertyRemoveOperator(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 try: client.remove(self.property_path) return {"FINISHED"} except: # NonAuthorizedOperationError: self.report( {'ERROR'}, "Non authorized operation") return {"CANCELLED"} class SessionPropertyRightOperator(bpy.types.Operator): bl_idname = "session.right" bl_label = "Change owner to" bl_description = "Change owner of specified datablock" 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 settings = context.window_manager.session col = layout.column() col.prop(settings, "clients") def execute(self, context): settings = context.window_manager.session global client if client: client.change_owner(self.key, settings.clients) return {"FINISHED"} class SessionSnapUserOperator(bpy.types.Operator): bl_idname = "session.snapview" bl_label = "snap to user" bl_description = "Snap 3d view to selected user" bl_options = {"REGISTER"} target_client: bpy.props.StringProperty(default="None") @classmethod def poll(cls, context): return True def execute(self, context): area, region, rv3d = presence.view3d_find() global client target_client = client.get(uuid=self.target_client) if target_client: rv3d.view_location = target_client.buffer['location'][0] rv3d.view_distance = 30.0 return {"FINISHED"} return {"CANCELLED"} class SessionApply(bpy.types.Operator): bl_idname = "session.apply" bl_label = "apply selected block into blender" bl_description = "Apply selected block into blender" bl_options = {"REGISTER"} target = bpy.props.StringProperty() @classmethod def poll(cls, context): return True def execute(self, context): global client client.apply(uuid=self.target) return {"FINISHED"} class SessionCommit(bpy.types.Operator): bl_idname = "session.commit" bl_label = "commit and push selected datablock to server" bl_description = "commit and push selected datablock to server" bl_options = {"REGISTER"} target = bpy.props.StringProperty() @classmethod def poll(cls, context): return True def execute(self, context): global client client.commit(uuid=self.target) client.push(uuid=self.target) return {"FINISHED"} @persistent def redresh_handler(dummy): """force to refresh client renderer """ global client if client: user = client.get(uuid=bpy.context.window_manager.session.user_uuid) if hasattr(user, "pointer"): user.pointer.is_dirty = True classes = ( SessionStartOperator, SessionStopOperator, SessionPropertyRemoveOperator, SessionSnapUserOperator, SessionPropertyRightOperator, SessionApply, SessionCommit, ) def register(): from bpy.utils import register_class for cls in classes: register_class(cls) bpy.app.handlers.depsgraph_update_post.append(redresh_handler) def unregister(): global client if client and client.state == 2: client.disconnect() client = None from bpy.utils import unregister_class for cls in reversed(classes): unregister_class(cls) bpy.app.handlers.depsgraph_update_post.remove(redresh_handler) if __name__ == "__main__": register()