diff --git a/multi_user/__init__.py b/multi_user/__init__.py index 2c8b800..4b6a42f 100644 --- a/multi_user/__init__.py +++ b/multi_user/__init__.py @@ -21,7 +21,7 @@ import sys import bpy from bpy.app.handlers import persistent -from . import environment, utils, presence +from . import environment, utils, presence, preferences from .libs.replication.replication.constants import RP_COMMON @@ -37,34 +37,12 @@ DEPENDENCIES = { logger = logging.getLogger(__name__) logger.setLevel(logging.WARNING) -#TODO: refactor config -# UTILITY FUNCTIONS -def generate_supported_types(): - stype_dict = {'supported_types':{}} - for type in bl_types.types_to_register(): - type_module = getattr(bl_types, type) - type_impl_name = "Bl{}".format(type.split('_')[1].capitalize()) - type_module_class = getattr(type_module, type_impl_name) - - props = {} - props['bl_delay_refresh']=type_module_class.bl_delay_refresh - props['bl_delay_apply']=type_module_class.bl_delay_apply - props['use_as_filter'] = False - props['icon'] = type_module_class.bl_icon - props['auto_push']=type_module_class.bl_automatic_push - props['bl_name']=type_module_class.bl_id - - stype_dict['supported_types'][type_impl_name] = props - - return stype_dict - - def client_list_callback(scene, context): from . import operators items = [(RP_COMMON, RP_COMMON, "")] - username = bpy.context.window_manager.session.username + username = bpy.context.preferences.addons[__package__].preferences.username cli = operators.client if cli: client_ids = cli.online_users.keys() @@ -77,23 +55,6 @@ def client_list_callback(scene, context): return items - -def randomColor(): - r = random.random() - v = random.random() - b = random.random() - return [r, v, b] - -class ReplicatedDatablock(bpy.types.PropertyGroup): - '''name = StringProperty() ''' - type_name: bpy.props.StringProperty() - bl_name: bpy.props.StringProperty() - bl_delay_refresh: bpy.props.FloatProperty() - bl_delay_apply: bpy.props.FloatProperty() - use_as_filter: bpy.props.BoolProperty(default=True) - auto_push: bpy.props.BoolProperty(default=True) - icon: bpy.props.StringProperty() - class SessionUser(bpy.types.PropertyGroup): """Session User @@ -104,37 +65,10 @@ class SessionUser(bpy.types.PropertyGroup): class SessionProps(bpy.types.PropertyGroup): - username: bpy.props.StringProperty( - name="Username", - default="user_{}".format(utils.random_string_digits()) - ) - ip: bpy.props.StringProperty( - name="ip", - description='Distant host ip', - default="127.0.0.1" - ) - user_uuid: bpy.props.StringProperty( - name="user_uuid", - default="None" - ) - port: bpy.props.IntProperty( - name="port", - description='Distant host port', - default=5555 - ) - ipc_port: bpy.props.IntProperty( - name="ipc_port", - description='internal ttl port(only usefull for multiple local instances)', - default=5561 - ) is_admin: bpy.props.BoolProperty( name="is_admin", default=False ) - start_empty: bpy.props.BoolProperty( - name="start_empty", - default=True - ) session_mode: bpy.props.EnumProperty( name='session_mode', description='session mode', @@ -142,17 +76,6 @@ class SessionProps(bpy.types.PropertyGroup): ('HOST', 'hosting', 'host a session'), ('CONNECT', 'connexion', 'connect to a session')}, default='HOST') - right_strategy: bpy.props.EnumProperty( - name='right_strategy', - description='right strategy', - items={ - ('STRICT', 'strict', 'strict right repartition'), - ('COMMON', 'common', 'relaxed right repartition')}, - default='COMMON') - client_color: bpy.props.FloatVectorProperty( - name="client_instance_color", - subtype='COLOR', - default=randomColor()) clients: bpy.props.EnumProperty( name="clients", description="client enum", @@ -175,12 +98,6 @@ class SessionProps(bpy.types.PropertyGroup): default=True, update=presence.update_overlay_settings ) - supported_datablock: bpy.props.CollectionProperty( - type=ReplicatedDatablock, - ) - session_filter: bpy.props.CollectionProperty( - type=ReplicatedDatablock, - ) filter_owned: bpy.props.BoolProperty( name="filter_owned", description='Show only owned datablocks', @@ -193,73 +110,13 @@ class SessionProps(bpy.types.PropertyGroup): default=False ) - def load(self): - config = environment.load_config() - if "username" in config.keys(): - self.username = config["username"] - self.ip = config["ip"] - self.port = config["port"] - self.start_empty = config["start_empty"] - self.enable_presence = config["enable_presence"] - self.client_color = config["client_color"] - else: - logger.error("Fail to read user config") - - if len(self.supported_datablock)>0: - self.supported_datablock.clear() - if "supported_types" not in config: - config = generate_supported_types() - for datablock in config["supported_types"].keys(): - rep_value = self.supported_datablock.add() - rep_value.name = datablock - rep_value.type_name = datablock - - config_block = config["supported_types"][datablock] - rep_value.bl_delay_refresh = config_block['bl_delay_refresh'] - rep_value.bl_delay_apply = config_block['bl_delay_apply'] - rep_value.icon = config_block['icon'] - rep_value.auto_push = config_block['auto_push'] - rep_value.bl_name = config_block['bl_name'] - - def save(self,context): - config = environment.load_config() - - if "supported_types" not in config: - config = generate_supported_types() - - config["username"] = self.username - config["ip"] = self.ip - config["port"] = self.port - config["start_empty"] = self.start_empty - config["enable_presence"] = self.enable_presence - config["client_color"] = [self.client_color.r,self.client_color.g,self.client_color.b] - - - for bloc in self.supported_datablock: - config_block = config["supported_types"][bloc.type_name] - config_block['bl_delay_refresh'] = bloc.bl_delay_refresh - config_block['bl_delay_apply'] = bloc.bl_delay_apply - config_block['use_as_filter'] = bloc.use_as_filter - config_block['icon'] = bloc.icon - config_block['auto_push'] = bloc.auto_push - config_block['bl_name'] = bloc.bl_name - environment.save_config(config) - - classes = ( SessionUser, - ReplicatedDatablock, SessionProps, - ) libs = os.path.dirname(os.path.abspath(__file__))+"\\libs\\replication\\replication" -@persistent -def load_handler(dummy): - import bpy - bpy.context.window_manager.session.load() - def register(): if libs not in sys.path: sys.path.append(libs) @@ -269,6 +126,7 @@ def register(): from . import presence from . import operators from . import ui + from . import preferences for cls in classes: bpy.utils.register_class(cls) @@ -280,22 +138,22 @@ def register(): type=SessionUser ) bpy.types.WindowManager.user_index = bpy.props.IntProperty() - bpy.context.window_manager.session.load() + preferences.register() presence.register() operators.register() ui.register() - bpy.app.handlers.load_post.append(load_handler) def unregister(): from . import presence from . import operators from . import ui + from . import preferences presence.unregister() ui.unregister() operators.unregister() - + preferences.unregister() del bpy.types.WindowManager.session for cls in reversed(classes): diff --git a/multi_user/delayable.py b/multi_user/delayable.py index c18c6e4..5a10cc2 100644 --- a/multi_user/delayable.py +++ b/multi_user/delayable.py @@ -88,7 +88,7 @@ class DynamicRightSelectTimer(Timer): def execute(self): session = operators.client - settings = bpy.context.window_manager.session + settings = bpy.context.preferences.addons[__package__].preferences if session and session.state['STATE'] == STATE_ACTIVE: # Find user @@ -220,8 +220,7 @@ class ClientUpdate(Timer): self.handle_quit = False def execute(self): - settings = bpy.context.window_manager.session - session_info = bpy.context.window_manager.session + settings = bpy.context.preferences.addons[__package__].preferences session = getattr(operators, 'client', None) renderer = getattr(presence, 'renderer', None) @@ -231,7 +230,7 @@ class ClientUpdate(Timer): bpy.ops.session.stop() local_user = operators.client.online_users.get( - session_info.username) + settings.username) if not local_user: return diff --git a/multi_user/environment.py b/multi_user/environment.py index d852762..6d8b49d 100644 --- a/multi_user/environment.py +++ b/multi_user/environment.py @@ -8,9 +8,6 @@ from pathlib import Path logger = logging.getLogger(__name__) logger.setLevel(logging.WARNING) -CONFIG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config") -CONFIG = os.path.join(CONFIG_DIR, "app.yaml") - THIRD_PARTY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "libs") CACHE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cache") PYTHON_PATH = None @@ -19,27 +16,6 @@ SUBPROCESS_DIR = None rtypes = [] - -def load_config(): - import yaml - - try: - with open(CONFIG, 'r') as config_file: - return yaml.safe_load(config_file) - except FileNotFoundError: - logger.info("no config") - return {} - - -def save_config(config): - import yaml - - logger.info("saving config") - - with open(CONFIG, 'w') as outfile: - yaml.dump(config, outfile, default_flow_style=False) - - def module_can_be_imported(name): try: __import__(name) @@ -68,9 +44,6 @@ def setup(dependencies, python_path): PYTHON_PATH = Path(python_path) SUBPROCESS_DIR = PYTHON_PATH.parent - check_dir(CACHE_DIR) - check_dir(CONFIG_DIR) - if not module_can_be_imported("pip"): install_pip() diff --git a/multi_user/operators.py b/multi_user/operators.py index 4ccd168..5a403af 100644 --- a/multi_user/operators.py +++ b/multi_user/operators.py @@ -58,14 +58,13 @@ class SessionStartOperator(bpy.types.Operator): def execute(self, context): global client, delayables, ui_context, server_process - settings = context.window_manager.session + settings = bpy.context.preferences.addons[__package__].preferences + runtime_settings = context.window_manager.session users = bpy.data.window_managers['WinMan'].online_users # TODO: Sync server clients users.clear() delayables.clear() - # save config - settings.save(context) bpy_factory = ReplicatedDataFactory() supported_bl_types = [] @@ -80,7 +79,7 @@ class SessionStartOperator(bpy.types.Operator): supported_bl_types.append(type_module_class.bl_id) # Retreive local replicated types settings - type_local_config = settings.supported_datablock[type_impl_name] + type_local_config = settings.supported_datablocks[type_impl_name] bpy_factory.register_type( type_module_class.bl_class, @@ -118,7 +117,7 @@ class SessionStartOperator(bpy.types.Operator): self.report({'ERROR'}, repr(e)) logger.error(f"Error: {e}") finally: - settings.is_admin = True + runtime_settings.is_admin = True # Join a session else: @@ -135,7 +134,7 @@ class SessionStartOperator(bpy.types.Operator): self.report({'ERROR'}, repr(e)) logger.error(f"Error: {e}") finally: - settings.is_admin = False + runtime_settings.is_admin = False # Background client updates service #TODO: Refactoring @@ -144,7 +143,7 @@ class SessionStartOperator(bpy.types.Operator): delayables.append(delayable.DynamicRightSelectTimer()) # Launch drawing module - if settings.enable_presence: + if runtime_settings.enable_presence: presence.renderer.run() # Register blender main thread tools @@ -226,17 +225,17 @@ class SessionPropertyRightOperator(bpy.types.Operator): def draw(self, context): layout = self.layout - settings = context.window_manager.session + runtime_settings = context.window_manager.session col = layout.column() - col.prop(settings, "clients") + col.prop(runtime_settings, "clients") def execute(self, context): - settings = context.window_manager.session + runtime_settings = context.window_manager.session global client if client: - client.change_owner(self.key, settings.clients) + client.change_owner(self.key, runtime_settings.clients) return {"FINISHED"} @@ -257,13 +256,13 @@ class SessionSnapUserOperator(bpy.types.Operator): def execute(self, context): wm = context.window_manager - settings = context.window_manager.session + runtime_settings = context.window_manager.session - if settings.time_snap_running: - settings.time_snap_running = False + if runtime_settings.time_snap_running: + runtime_settings.time_snap_running = False return {'CANCELLED'} else: - settings.time_snap_running = True + runtime_settings.time_snap_running = True self._timer = wm.event_timer_add(0.1, window=context.window) wm.modal_handler_add(self) @@ -311,13 +310,13 @@ class SessionSnapTimeOperator(bpy.types.Operator): return True def execute(self, context): - settings = context.window_manager.session + runtime_settings = context.window_manager.session - if settings.user_snap_running: - settings.user_snap_running = False + if runtime_settings.user_snap_running: + runtime_settings.user_snap_running = False return {'CANCELLED'} else: - settings.user_snap_running = True + runtime_settings.user_snap_running = True wm = context.window_manager self._timer = wm.event_timer_add(0.05, window=context.window) @@ -483,7 +482,7 @@ def depsgraph_evaluation(scene): context = bpy.context blender_depsgraph = bpy.context.view_layer.depsgraph dependency_updates = [u for u in blender_depsgraph.updates] - session_infos = bpy.context.window_manager.session + session_infos = bpy.context.preferences.addons[__package__].preferences # NOTE: maybe we don't need to check each update but only the first diff --git a/multi_user/preferences.py b/multi_user/preferences.py new file mode 100644 index 0000000..c7ccaa7 --- /dev/null +++ b/multi_user/preferences.py @@ -0,0 +1,181 @@ +import logging +import bpy + +from . import utils, bl_types + +logger = logging.getLogger(__name__) + +class ReplicatedDatablock(bpy.types.PropertyGroup): + type_name: bpy.props.StringProperty() + bl_name: bpy.props.StringProperty() + bl_delay_refresh: bpy.props.FloatProperty() + bl_delay_apply: bpy.props.FloatProperty() + use_as_filter: bpy.props.BoolProperty(default=True) + auto_push: bpy.props.BoolProperty(default=True) + icon: bpy.props.StringProperty() + +class SessionPrefs(bpy.types.AddonPreferences): + bl_idname = __package__ + + ip: bpy.props.StringProperty( + name="ip", + description='Distant host ip', + default="127.0.0.1") + username: bpy.props.StringProperty( + name="Username", + default="user_{}".format(utils.random_string_digits()) + ) + client_color: bpy.props.FloatVectorProperty( + name="client_instance_color", + subtype='COLOR', + default=utils.randomColor()) + port: bpy.props.IntProperty( + name="port", + description='Distant host port', + default=5555 + ) + supported_datablocks: bpy.props.CollectionProperty( + type=ReplicatedDatablock, + ) + ipc_port: bpy.props.IntProperty( + name="ipc_port", + description='internal ttl port(only usefull for multiple local instances)', + default=5561 + ) + start_empty: bpy.props.BoolProperty( + name="start_empty", + default=False + ) + right_strategy: bpy.props.EnumProperty( + name='right_strategy', + description='right strategy', + items={ + ('STRICT', 'strict', 'strict right repartition'), + ('COMMON', 'common', 'relaxed right repartition')}, + default='COMMON') + # for UI + category: bpy.props.EnumProperty( + name="Category", + description="Preferences Category", + items=[ + ('INFO', "Information", "Information about this add-on"), + ('CONFIG', "Configuration", "Configuration about this add-on"), + # ('UPDATE', "Update", "Update this add-on"), + ], + default='INFO' + ) + conf_session_identity_expanded: bpy.props.BoolProperty( + name="Identity", + description="Identity", + default=True + ) + conf_session_rights_expanded: bpy.props.BoolProperty( + name="Rights", + description="Rights", + default=True + ) + conf_session_net_expanded: bpy.props.BoolProperty( + name="Net", + description="net", + default=True + ) + + + def draw(self, context): + layout = self.layout + + layout.row().prop(self, "category", expand=True) + + if self.category == 'INFO': + layout.separator() + layout.label(text="WIP") + if self.category == 'CONFIG': + grid = layout.column() + + # USER INFORMATIONS + box = grid.box() + box.prop( + self, "conf_session_identity_expanded", text="User informations", + icon='DISCLOSURE_TRI_DOWN' if self.conf_session_identity_expanded + else 'DISCLOSURE_TRI_RIGHT') + if self.conf_session_identity_expanded: + box.row().prop(self, "username", text="name") + box.row().prop(self, "client_color", text="color") + + # NETWORK SETTINGS + box = grid.box() + box.prop( + self, "conf_session_net_expanded", text="Netorking", + icon='DISCLOSURE_TRI_DOWN' if self.conf_session_net_expanded + else 'DISCLOSURE_TRI_RIGHT') + if self.conf_session_net_expanded: + box.row().prop(self, "ip", text="Address") + row = box.row() + row.label(text="Port:") + row.prop(self, "port", text="Address") + row = box.row() + row.label(text="Start with an empty scene:") + row.prop(self, "start_empty", text="") + + line = box.row(align=True) + line.label(text=" ") + line.separator() + line.label(text="refresh (sec)") + line.label(text="apply (sec)") + + for item in self.supported_datablocks: + line = box.row(align=True) + line.label(text="", icon=item.icon) + line.prop(item, "bl_delay_refresh", text="") + line.prop(item, "bl_delay_apply", text="") + # HOST SETTINGS + box = grid.box() + box.prop( + self, "conf_session_rights_expanded", text="Hosting", + icon='DISCLOSURE_TRI_DOWN' if self.conf_session_rights_expanded + else 'DISCLOSURE_TRI_RIGHT') + if self.conf_session_rights_expanded: + box.row().prop(self, "right_strategy", text="Right model") + row = box.row() + row.label(text="Start with an empty scene:") + row.prop(self, "start_empty", text="") + + def generate_supported_types(self): + self.supported_datablocks.clear() + + for type in bl_types.types_to_register(): + new_db = self.supported_datablocks.add() + + type_module = getattr(bl_types, type) + type_impl_name = "Bl{}".format(type.split('_')[1].capitalize()) + type_module_class = getattr(type_module, type_impl_name) + + new_db.name = type_impl_name + new_db.type_name = type_impl_name + new_db.bl_delay_refresh = type_module_class.bl_delay_refresh + new_db.bl_delay_apply =type_module_class.bl_delay_apply + new_db.use_as_filter = True + new_db.icon = type_module_class.bl_icon + new_db.auto_push =type_module_class.bl_automatic_push + new_db.bl_name=type_module_class.bl_id + +classes = ( + ReplicatedDatablock, + SessionPrefs, +) +def register(): + from bpy.utils import register_class + + for cls in classes: + register_class(cls) + + prefs = bpy.context.preferences.addons[__package__].preferences + if len(prefs.supported_datablocks) == 0: + logger.info('Generating bl_types preferences') + prefs.generate_supported_types() + +def unregister(): + from bpy.utils import unregister_class + + for cls in reversed(classes): + unregister_class(cls) \ No newline at end of file diff --git a/multi_user/presence.py b/multi_user/presence.py index 1a739f0..309bf87 100644 --- a/multi_user/presence.py +++ b/multi_user/presence.py @@ -204,7 +204,7 @@ class DrawFactory(object): self.d2d_items.clear() def draw_client_selection(self, client_id, client_color, client_selection): - local_user = bpy.context.window_manager.session.username + local_user = bpy.context.preferences.addons[__package__].preferences.username if local_user != client_id: self.flush_selection(client_id) @@ -261,7 +261,7 @@ class DrawFactory(object): def draw_client_camera(self, client_id, client_location, client_color): if client_location: - local_user = bpy.context.window_manager.session.username + local_user = bpy.context.preferences.addons[__package__].preferences.username if local_user != client_id: try: diff --git a/multi_user/ui.py b/multi_user/ui.py index 9513565..d68402d 100644 --- a/multi_user/ui.py +++ b/multi_user/ui.py @@ -138,12 +138,13 @@ class SESSION_PT_settings_network(bpy.types.Panel): def draw(self, context): layout = self.layout - - settings = context.window_manager.session + + runtime_settings = context.window_manager.session + settings = bpy.context.preferences.addons[__package__].preferences # USER SETTINGS row = layout.row() - row.prop(settings, "session_mode", expand=True) + row.prop(runtime_settings, "session_mode", expand=True) row = layout.row() box = row.box() @@ -157,7 +158,7 @@ class SESSION_PT_settings_network(bpy.types.Panel): row.label(text="IPC Port:") row.prop(settings, "ipc_port", text="") - if settings.session_mode == 'HOST': + if runtime_settings.session_mode == 'HOST': row = box.row() row.label(text="Start empty:") row.prop(settings, "start_empty", text="") @@ -184,8 +185,9 @@ class SESSION_PT_settings_user(bpy.types.Panel): def draw(self, context): layout = self.layout - settings = context.window_manager.session - + runtime_settings = context.window_manager.session + settings = bpy.context.preferences.addons[__package__].preferences + row = layout.row() # USER SETTINGS row.prop(settings, "username", text="name") @@ -212,9 +214,11 @@ class SESSION_PT_settings_replication(bpy.types.Panel): def draw(self, context): layout = self.layout - settings = context.window_manager.session + runtime_settings = context.window_manager.session + settings = bpy.context.preferences.addons[__package__].preferences + # Right managment - if settings.session_mode == 'HOST': + if runtime_settings.session_mode == 'HOST': row = layout.row(align=True) row.label(text="Right strategy:") row.prop(settings,"right_strategy",text="") @@ -231,7 +235,7 @@ class SESSION_PT_settings_replication(bpy.types.Panel): line.label(text="refresh (sec)") line.label(text="apply (sec)") - for item in settings.supported_datablock: + for item in settings.supported_datablocks: line = flow.row(align=True) line.prop(item, "auto_push", text="", icon=item.icon) line.separator() @@ -255,7 +259,7 @@ class SESSION_PT_user(bpy.types.Panel): layout = self.layout online_users = context.window_manager.online_users selected_user = context.window_manager.user_index - settings = context.window_manager.session + settings = bpy.context.preferences.addons[__package__].preferences active_user = online_users[selected_user] if len(online_users)-1>=selected_user else 0 @@ -289,7 +293,7 @@ class SESSION_PT_user(bpy.types.Panel): class SESSION_UL_users(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag): session = operators.client - settings = context.window_manager.session + settings = bpy.context.preferences.addons[__package__].preferences is_local_user = item.username == settings.username ping = '-' frame_current = '-' @@ -364,7 +368,8 @@ class SESSION_PT_services(bpy.types.Panel): def draw_property(context, parent, property_uuid, level=0): - settings = context.window_manager.session + settings = bpy.context.preferences.addons[__package__].preferences + runtime_settings = context.window_manager.session item = operators.client.get(uuid=property_uuid) if item.state == ERROR: @@ -381,12 +386,12 @@ def draw_property(context, parent, property_uuid, level=0): detail_item_box = line.row(align=True) detail_item_box.label(text="", - icon=settings.supported_datablock[item.str_type].icon) + icon=settings.supported_datablocks[item.str_type].icon) detail_item_box.label(text="{} ".format(name)) # Operations - have_right_to_modify = settings.is_admin or \ + have_right_to_modify = runtime_settings.is_admin or \ item.owner == settings.username or \ item.owner == RP_COMMON @@ -444,7 +449,8 @@ class SESSION_PT_outliner(bpy.types.Panel): if hasattr(context.window_manager, 'session'): # Filters - settings = context.window_manager.session + settings = bpy.context.preferences.addons[__package__].preferences + runtime_settings = context.window_manager.session flow = layout.grid_flow( row_major=True, columns=0, @@ -452,21 +458,21 @@ class SESSION_PT_outliner(bpy.types.Panel): even_rows=False, align=True) - for item in settings.supported_datablock: + for item in settings.supported_datablocks: col = flow.column(align=True) col.prop(item, "use_as_filter", text="", icon=item.icon) row = layout.row(align=True) - row.prop(settings, "filter_owned", text="Show only owned") + row.prop(runtime_settings, "filter_owned", text="Show only owned") row = layout.row(align=True) # Properties - types_filter = [t.type_name for t in settings.supported_datablock + types_filter = [t.type_name for t in settings.supported_datablocks if t.use_as_filter] key_to_filter = operators.client.list( - filter_owner=settings.username) if settings.filter_owned else operators.client.list() + filter_owner=settings.username) if runtime_settings.filter_owned else operators.client.list() client_keys = [key for key in key_to_filter if operators.client.get(uuid=key).str_type diff --git a/multi_user/utils.py b/multi_user/utils.py index 08f1727..f9662c9 100644 --- a/multi_user/utils.py +++ b/multi_user/utils.py @@ -37,7 +37,7 @@ def find_from_attr(attr_name, attr_value, list): def get_datablock_users(datablock): users = [] - supported_types = bpy.context.window_manager.session.supported_datablock + supported_types = bpy.context.preferences.addons[__package__].preferences.supported_datablocks if hasattr(datablock, 'users_collection') and datablock.users_collection: users.extend(list(datablock.users_collection)) if hasattr(datablock, 'users_scene') and datablock.users_scene: @@ -59,6 +59,11 @@ def random_string_digits(stringLength=6): lettersAndDigits = string.ascii_letters + string.digits return ''.join(random.choices(lettersAndDigits, k=stringLength)) +def randomColor(): + r = random.random() + v = random.random() + b = random.random() + return [r, v, b] def clean_scene(): for type_name in dir(bpy.data):