''' Copyright (C) 2023 CG Cookie http://cgcookie.com hello@cgcookie.com Created by Jonathan Denning, Jonathan Williamson This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ''' import os import re import sys import glob import time import inspect import operator import itertools import importlib import bpy from mathutils import Vector, Matrix from .blender_preferences import get_preferences from .profiler import profiler from .debug import dprint, debugger def normalize_triplequote( s, *, remove_trailing_spaces=True, remove_first_leading_newline=True, remove_all_leading_newlines=False, dedent=True, remove_all_trailing_newlines=True, ensure_trailing_newline=True, ): ''' todo: - (re)wrap text to given line length - sub '\n\n' for '\n\n\n+' - replace HTML chars with UTF? ''' if remove_trailing_spaces: s = '\n'.join(l.rstrip() for l in s.splitlines()) if remove_all_leading_newlines: s = re.sub(r'^\n+', '', s) elif remove_first_leading_newline: s = re.sub(r'^\n', '', s) if dedent: lines = s.splitlines() indent = min((len(line) - len(line.lstrip()) for line in lines if line.lstrip()), default=0) s = '\n'.join(line[indent:] for line in lines) if remove_all_trailing_newlines: s = re.sub(r'\n+$', '', s) if ensure_trailing_newline: if not s.endswith('\n'): s += '\n' return s ################################################## StructRNA = bpy.types.bpy_struct def still_registered(self, oplist): if getattr(still_registered, 'is_broken', False): return False def is_registered(): cur = bpy.ops for n in oplist: if not hasattr(cur, n): return False cur = getattr(cur, n) try: StructRNA.path_resolve(self, "properties") except: print('no properties!') return False return True if is_registered(): return True still_registered.is_broken = True print('bpy.ops.%s is no longer registered!' % '.'.join(oplist)) return False registered_objects = {} def registered_object_add(self): global registered_objects opid = self.operator_id print('Registering bpy.ops.%s' % opid) registered_objects[opid] = (self, opid.split('.')) def registered_check(): global registered_objects return all(still_registered(s, o) for (s, o) in registered_objects.values()) ################################################# # def find_and_import_all_subclasses(cls, root_path=None): # here_path = os.path.realpath(os.path.dirname(__file__)) # if root_path is None: # root_path = os.path.realpath(os.path.join(here_path, '..')) # touched_paths = set() # found_subclasses = set() # def search(root): # nonlocal touched_paths, found_subclasses, here_path # root = os.path.realpath(root) # if root in touched_paths: return # touched_paths.add(root) # relpath = os.path.relpath(root, here_path) # #print(' relpath: %s' % relpath) # for path in glob.glob(os.path.join(root, '*')): # if os.path.isdir(path): # if not path.endswith('__pycache__'): # search(path) # continue # if os.path.splitext(path)[1] != '.py': # continue # try: # pyfile = os.path.splitext(os.path.basename(path))[0] # if pyfile == '__init__': continue # pyfile = os.path.join(relpath, pyfile) # pyfile = re.sub(r'\\', '/', pyfile) # if pyfile.startswith('./'): pyfile = pyfile[2:] # level = pyfile.count('..') # pyfile = re.sub(r'^(\.\./)*', '', pyfile) # pyfile = re.sub('/', '.', pyfile) # #print(' Searching: %s (%d, %s)' % (pyfile, level, path)) # try: # tmp = importlib.__import__(pyfile, globals(), locals(), [], level=level+1) # except Exception as e: # print('Caught exception while attempting to search for classes') # print(' cls: %s' % str(cls)) # print(' pyfile: %s' % pyfile) # print(' %s' % str(e)) # #print(' Could not import') # continue # for tk in dir(tmp): # m = getattr(tmp, tk) # if not inspect.ismodule(m): continue # for k in dir(m): # v = getattr(m, k) # if not inspect.isclass(v): continue # if v is cls: continue # if not issubclass(v, cls): continue # # v is a subclass of cls, so add it to the global namespace # #print(' Found %s in %s' % (str(v), pyfile)) # globals()[k] = v # found_subclasses.add(v) # except Exception as e: # print('Exception occurred while searching %s' % path) # debugger.print_exception() # #print('Searching for class %s' % str(cls)) # #print(' cwd: %s' % os.getcwd()) # #print(' Root: %s' % root_path) # search(root_path) # return found_subclasses ######################################################### def delay_exec(action, f_globals=None, f_locals=None, ordered_parameters=None, precall=None): if f_globals is None or f_locals is None: frame = inspect.currentframe().f_back # get frame of calling function if f_globals is None: f_globals = frame.f_globals # get globals of calling function if f_locals is None: f_locals = frame.f_locals # get locals of calling function def run_it(*args, **kwargs): # args are ignored!? nf_locals = dict(f_locals) if ordered_parameters: for k,v in zip(ordered_parameters, args): nf_locals[k] = v nf_locals.update(kwargs) try: if precall: precall(nf_locals) return exec(action, f_globals, nf_locals) except Exception as e: print('Caught exception while trying to run a delay_exec') print(' action:', action) print(' except:', e) raise e return run_it ######################################################### # def git_info(start_at_caller=True): # if start_at_caller: # path_root = os.path.abspath(inspect.stack()[1][1]) # else: # path_root = os.path.abspath(os.path.dirname(__file__)) # try: # path_git_head = None # while path_root: # path_test = os.path.join(path_root, '.git', 'HEAD') # if os.path.exists(path_test): # # found it! # path_git_head = path_test # break # if os.path.split(path_root)[1] in {'addons', 'addons_contrib'}: # break # path_root = os.path.dirname(path_root) # try next level up # if not path_git_head: # # could not find .git folder # return None # path_git_ref = open(path_git_head).read().split()[1] # if not path_git_ref.startswith('refs/heads/'): # print('git detected, but HEAD uses unexpected format') # return None # path_git_ref = path_git_ref[len('refs/heads/'):] # git_ref_fullpath = os.path.join(path_root, '.git', 'logs', 'refs', 'heads', path_git_ref) # if not os.path.exists(git_ref_fullpath): # print('git detected, but could not find ref file %s' % git_ref_fullpath) # return None # log = open(git_ref_fullpath).read().splitlines() # commit = log[-1].split()[1] # return ('%s %s' % (path_git_ref, commit)) # except Exception as e: # print('An exception occurred while checking git info') # print(e) # return None ######################################################### def kwargopts(kwargs, defvals=None, **mykwargs): opts = defvals.copy() if defvals else {} opts.update(mykwargs) opts.update(kwargs) if 'opts' in kwargs: opts.update(opts['opts']) def factory(): class Opts(): ''' pretend to be a dictionary, but also add . access fns ''' def __init__(self): self.touched = set() def __getattr__(self, opt): self.touched.add(opt) return opts[opt] def __getitem__(self, opt): self.touched.add(opt) return opts[opt] def __len__(self): return len(opts) def has_key(self, opt): return opt in opts def keys(self): return opts.keys() def values(self): return opts.values() def items(self): return opts.items() def __contains__(self, opt): return opt in opts def __iter__(self): return iter(opts) def print_untouched(self): print('untouched: %s' % str(set(opts.keys()) - self.touched)) def pass_through(self, *args): return {key:self[key] for key in args} return Opts() return factory() def kwargs_translate(key_from, key_to, kwargs): if key_from in kwargs: kwargs[key_to] = kwargs[key_from] del kwargs[key_from] def kwargs_splitter(kwargs, *, keys=None, fn=None): if keys is not None: if type(keys) is str: keys = [keys] kw = {k:v for (k,v) in kwargs.items() if k in keys} elif fn is not None: kw = {k:v for (k,v) in kwargs.items() if fn(k, v)} else: assert False, f'Must specify either keys or fn' for k in kw.keys(): del kwargs[k] return kw def any_args(*args): return any(bool(a) for a in args) def get_and_discard(d, k, default=None): if k not in d: return default v = d[k] del d[k] return v ################################################# def abspath(*args, frame_depth=1, **kwargs): frame = inspect.currentframe() for i in range(frame_depth): frame = frame.f_back module = inspect.getmodule(frame) path = os.path.dirname(module.__file__) return os.path.abspath(os.path.join(path, *args, **kwargs)) ################################################# def strshort(s, l=50): s = str(s) return s[:l] + ('...' if len(s) > l else '') def join(sep, iterable, preSep='', postSep='', toStr=str): ''' this function adds features on to sep.join(iterable) if iterable is not empty, preSep is prepended and postSep is appended also, all items of iterable are turned to strings using toStr, which can be customized ex: join(', ', [1,2,3]) => '1, 2, 3' ex: join('.', ['foo', 'bar'], preSep='.') => '.foo.bar' ''' s = sep.join(map(toStr, iterable)) if not s: return '' return f'{preSep}{s}{postSep}' def accumulate_last(iterable, *args, **kwargs): # returns last result when accumulating # https://docs.python.org/3.7/library/itertools.html#itertools.accumulate final = None for step in itertools.accumulate(iterable, *args, **kwargs): final = step return final def selection_mouse(): select_type = get_preferences().inputs.select_mouse return ['%sMOUSE' % select_type, 'SHIFT+%sMOUSE' % select_type] def get_settings(): if not hasattr(get_settings, 'cache'): addons = get_preferences().addons folderpath = os.path.dirname(os.path.abspath(__file__)) while folderpath: folderpath,foldername = os.path.split(folderpath) if foldername in {'lib','addons', 'addons_contrib'}: continue if foldername in addons: break else: assert False, 'Could not find non-"lib" folder' if not addons[foldername].preferences: return None get_settings.cache = addons[foldername].preferences return get_settings.cache def get_dpi(): system_preferences = get_preferences().system factor = getattr(system_preferences, "pixel_size", 1) return int(system_preferences.dpi * factor) def get_dpi_factor(): return get_dpi() / 72 def blender_version(): major,minor,rev = bpy.app.version # '%03d.%03d.%03d' % (major, minor, rev) return '%d.%02d' % (major,minor) def iter_head(iterable, *, default=None): return next(iter(iterable), default) try: return next(iter(iterable)) except StopIteration: return default def iter_running_sum(lw): s = 0 for w in lw: s += w yield (w,s) def iter_pairs(items, wrap, repeat=False): if not items: return while True: for i0,i1 in zip(items[:-1],items[1:]): yield i0,i1 if wrap: yield items[-1],items[0] if not repeat: return def rotate_cycle(cycle, offset): l = len(cycle) return [cycle[(l + ((i - offset) % l)) % l] for i in range(l)] def max_index(vals, key=None): if not key: return max(enumerate(vals), key=lambda ival:ival[1])[0] return max(enumerate(vals), key=lambda ival:key(ival[1]))[0] def min_index(vals, key=None): if not key: return min(enumerate(vals), key=lambda ival:ival[1])[0] return min(enumerate(vals), key=lambda ival:key(ival[1]))[0] def shorten_floats(s): # reduces number of digits (for float) found in a string # useful for reducing noise of printing out a Vector, Buffer, Matrix, etc. s = re.sub(r'(?P-?)(?P\d)\.(?P\d)\d\d+e-02', r'\g0.0\g\g', s) s = re.sub(r'(?P-?)(?P\d)\.\d\d\d+e-03', r'\g0.00\g', s) s = re.sub(r'-?\d\.\d\d\d+e-0[4-9]', r'0.000', s) s = re.sub(r'-?\d\.\d\d\d+e-[1-9]\d', r'0.000', s) s = re.sub(r'(?P\d\.\d\d\d)\d+', r'\g', s) return s def get_matrices(ob): ''' obtain blender object matrices ''' mx = ob.matrix_world imx = mx.inverted_safe() return (mx, imx) class AddonLocator(object): def __init__(self, f=None): self.fullInitPath = f if f else __file__ self.FolderPath = os.path.dirname(self.fullInitPath) self.FolderName = os.path.basename(self.FolderPath) def AppendPath(self): sys.path.append(self.FolderPath) print("Addon path has been registered into system path for this session") class UniqueCounter(): __counter = 0 @staticmethod def next(): UniqueCounter.__counter += 1 return UniqueCounter.__counter class Dict(): ''' a fancy dictionary object ''' def __init__(self, *args, **kwargs): self.__dict__['__d'] = {} if 'get_default' in kwargs: v = kwargs.pop('get_default') self.set_get_default_fn(lambda: v) if 'get_default_fn' in kwargs: self.set_get_default_fn(kwargs.pop('get_default_fn')) self.set(*args, **kwargs) def set_get_default_fn(self, fn): self.__dict__['__default_fn'] = fn def __getitem__(self, k): d = self.__dict__['__d'] if '__default_fn' in self.__dict__: # get_default_fn set return d[k] if k in d else self.__dict__['__default_fn']() return self.__dict__['__d'][k] def get(self, k, *args): d = self.__dict__['__d'] if args: # default specified return d.get(k, *args) if '__default_fn' in self.__dict__: # get_default_fn set return d[k] if k in d else self.__dict__['__default_fn']() return d.get(k) def __setitem__(self, k, v): self.__dict__['__d'][k] = v return v def __delitem__(self, k): del self.__dict__['__d'][k] def __getattr__(self, k): return self.get(k) # return self.__dict__['__d'][k] def __setattr__(self, k, v): self.__dict__['__d'][k] = v return v def __delattr__(self, k): del self.__dict__['__d'][k] def set(self, kvs=None, **kwargs): kvs = kvs or {} for k,v in itertools.chain(kvs.items(), kwargs.items()): self[k] = v def __str__(self): return str(self.__dict__['__d']) def __repr__(self): return repr(self.__dict__['__d']) def values(self): return self.__dict__['__d'].values() def __iter__(self): return iter(self.__dict__['__d']) def has_duplicates(lst): l = len(lst) if l == 0: return False if l < 20 or not hasattr(lst[0], '__hash__'): # runs in O(n^2) time (perfectly fine if n is small, assuming [:index] uses iter) # does not require items in list to hash # requires O(1) memory return any(item in lst[:index] for (index,item) in enumerate(lst)) else: # runs in either O(n) time (assuming hash-set) # requires items to hash # requires O(N) memory seen = set() for i in lst: if i in seen: return True seen.add(i) return False def deduplicate_list(l): nl = [] for i in l: if i in nl: continue nl.append(i) return nl class StopWatch: def __init__(self): self._start = time.time() self._last = time.time() def elapsed(self): self._last, prev = time.time(), self._last return self._last - prev def total_elapsed(self): return time.time() - self._start