''' 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 math import inspect from inspect import ismethod, isfunction, signature from collections import namedtuple from contextlib import contextmanager import bpy import bpy.utils.previews from .decorators import blender_version_wrapper, only_in_blender_version, add_cache, ignore_exceptions from .functools import find_fns, self_wrapper from .blender_cursors import Cursors from ..terminal import term_printer def get_view3d_area(context=None): # assuming: context.screen is correct, and a SINGLE VIEW_3D area! if not context: context = bpy.context if context.area and context.area.type == 'VIEW_3D': return context.area return next((a for a in context.screen.areas if a.type == 'VIEW_3D'), None) def get_view3d_region(context=None): if not context: context = bpy.context if context.region and context.region.type == 'WINDOW': return context.region area = get_view3d_area(context=context) return next((r for r in area.regions if r.type == 'WINDOW'), None) if area else None def get_view3d_space(context=None): if not context: context = bpy.context if context.space_data and context.space_data.type == 'VIEW_3D': return context.space_data area = get_view3d_area(context=context) return next((s for s in area.spaces if s.type == 'VIEW_3D'), None) if area else None class StoreRestore: def __init__(self, *, init_storage=None): self._bindings = {} self._bind_order = [] self._storage = {} self._restoring = False self._callbacks = [] self._delay = False self._delayed = False if init_storage: self.init_storage(init_storage) def init_storage(self, storage, *, update_only=False): if update_only: for k in storage: self._storage[k] = storage[k] else: self._storage = storage def bind(self, key, fn_get, fn_set, fn_restore=None): def fn_set_wrapper(): def wrapped(*args, **kwargs): # print(f'SETTING {key} {args} {kwargs} {self._restoring} {fn_set} {fn_restore}') if self._restoring and fn_restore: return fn_restore(*args, **kwargs) return fn_set(*args, **kwargs) return wrapped self._bindings[key] = (fn_get, fn_set_wrapper()) self._bind_order.append(key) def bind_all(self, iter_key_get_set_optrestore): for (key, *fns) in iter_key_get_set_optrestore: self.bind(key, *fns) def clear_storage_change_callbacks(self): self._callbacks.clear() def register_storage_change_callback(self, fn): if fn in self._callbacks: return self._callbacks.append(fn) self.call_storage_change_callback() @contextmanager def delay_storage_change_callback(self): try: self._delay = True self._delayed = False yield None finally: # print(f'$$ {self._delayed=}') self._delay = False if self._delayed: self.call_storage_change_callback() def call_storage_change_callback(self): if not self._callbacks: return if self._delay: self._delayed = True else: for fn in self._callbacks: fn(self._storage) def __setitem__(self, k, v): self.set(k, v) def set(self, k, v): self.store(k, only_new=True) _, fn_set = self._bindings[k] fn_set(v) def __getitem__(self, k): return self.get(k) def get(self, k): fn_get, _ = self._bindings[k] return fn_get() def store(self, k, *, only_new=True): if only_new and k in self._storage: return fn_get, _ = self._bindings[k] nv = fn_get() if k in self._storage: # print(f'>> {k=} {self._storage[k]=} {nv=}') if self._storage[k] == nv: return else: # print(f'++ {k=} {nv=}') pass self._storage[k] = nv self.call_storage_change_callback() def store_all(self, *, only_new=True): with self.delay_storage_change_callback(): for k in self._bindings: self.store(k, only_new=only_new) def discard(self, k): # print(f'-- discard({k=})') self._storage.discard(k) self.call_storage_change_callback() def remove(self, k): # print(f'-- remove({k=})') self._storage.remove(k) self.call_storage_change_callback() def restore(self, k, *, discard=False): if k not in self._storage: return if k not in self._bindings: print(f'Addon Common: Could not find setter for {k}') else: # print(f'Addon Common: Restoring {k} = {self._storage[k]}') _, fn_set = self._bindings[k] try: self._restoring = True fn_set(self._storage[k]) finally: self._restoring = False if discard: self.discard(k) def restore_all(self, *, ignore=None, discard=False): ignore = ignore or set() with self.delay_storage_change_callback(): for k in self._bind_order: if k not in self._storage or k in ignore: continue self.restore(k, discard=discard) class BlenderSettings: ######################################### # Workspace and Scene @staticmethod def workspace_get(): return bpy.context.window.workspace.name @staticmethod def workspace_set(name): bpy.context.window.workspace = bpy.data.workspaces[name] @staticmethod def scene_get(): return bpy.context.window.scene.name @staticmethod def scene_set(name): bpy.context.window.scene = bpy.data.scenes[name] @staticmethod def scene_scale_get(): return bpy.context.scene.unit_settings.scale_length @staticmethod def scene_scale_set(v): bpy.context.scene.unit_settings.scale_length = v ######################################### # Objects # NOTE: select, active, and visible properties are stored in scene! @staticmethod def objects_selected_get(): return [ o.name for o in bpy.data.objects if o.select_get() ] @staticmethod def objects_selected_restore(names): BlenderSettings.objects_selected_set(names, only=True) @staticmethod def objects_selected_set(names, *, only=False): names = set(names) for o in bpy.data.objects: if only: o.select_set(o.name in names) elif o.name in names: o.select_set(True) @staticmethod def objects_visible_get(): return [ o.name for o in bpy.data.objects if not o.hide_viewport ] #hide_get() ] @staticmethod def objects_visible_restore(names): BlenderSettings.objects_visible_set(names, only=True) @staticmethod def objects_visible_set(names, *, only=False): names = set(names) for o in bpy.data.objects: if only: o.hide_viewport = (o.name not in names) #hide_set(o.name not in names) elif o.name in names: o.hide_viewport = False # hide_set(False) @staticmethod def object_active_get(): return bpy.context.view_layer.objects.active.name if bpy.context.view_layer.objects.active else None @staticmethod def object_active_set(name): if not name: return obj = bpy.data.objects[name] obj.select_set(True) bpy.context.view_layer.objects.active = obj ######################################### # Header, Status Bar, Cursor @staticmethod def header_text_set(s=None): get_view3d_area().header_text_set(text=s) @staticmethod def header_text_restore(): BlenderSettings.header_text_set() @staticmethod def statusbar_text_set(s=None, *, internal=False): if not internal: bpy.context.workspace.status_text_set(text=s) else: bpy.context.workspace.status_text_set_internal(text=s) @staticmethod def statusbar_text_restore(): BlenderSettings.statusbar_text_set() @staticmethod def cursor_set(cursor): Cursors.set(cursor) @staticmethod def cursor_restore(): Cursors.restore() ######################################### # Region Panels @staticmethod def _get_region(*, label=None, type=None): if label: type = region_label_to_data[label].type area = get_view3d_area() return next((r for r in area.regions if r.type == type), None) @staticmethod def _get_regions(): return { label: BlenderSettings._get_region(label=label) for label in region_label_to_data } @staticmethod def panels_get(): rgns = BlenderSettings._get_regions() return { label: (rgns[label].width > 1 and rgns[label].height > 1) if rgns[label] else False for label in region_label_to_data } @staticmethod def panels_set(state): ctx = create_simple_context(bpy.context) current = BlenderSettings.panels_get() for label, val in state.items(): if val == current[label]: continue fn_toggle = region_label_to_data[label].fn_toggle if fn_toggle: fn_toggle(ctx) @staticmethod def panels_hide(*, ignore=None): ignore = ignore or set() BlenderSettings.panels_set({ label: False for label in region_label_to_data if label not in ignore }) ######################################### # Viewport Shading and Settings @staticmethod def shading_type_get(): return get_view3d_space().shading.type @staticmethod def shading_type_set(v): get_view3d_space().shading.type = v @staticmethod def shading_light_get(): return get_view3d_space().shading.light @staticmethod def shading_light_set(v): get_view3d_space().shading.light = v @staticmethod def shading_matcap_get(): return get_view3d_space().shading.studio_light @staticmethod @ignore_exceptions(TypeError) # ignore type error (enum value doesn't exist in this context) def shading_matcap_set(v): get_view3d_space().shading.studio_light = v @staticmethod def shading_colortype_get(): return get_view3d_space().shading.color_type @staticmethod @ignore_exceptions(TypeError) # ignore type error (enum value doesn't exist in this context) def shading_colortype_set(v): get_view3d_space().shading.color_type = v @staticmethod def shading_color_get(): return get_view3d_space().shading.single_color @staticmethod def shading_color_set(v): get_view3d_space().shading.single_color = v @staticmethod def shading_backface_get(): return get_view3d_space().shading.show_backface_culling @staticmethod def shading_backface_set(v): get_view3d_space().shading.show_backface_culling = v @staticmethod def shading_shadows_get(): return get_view3d_space().shading.show_shadows @staticmethod def shading_shadows_set(v): get_view3d_space().shading.show_shadows = v @staticmethod def shading_xray_get(): return get_view3d_space().shading.show_xray @staticmethod def shading_xray_set(v): get_view3d_space().shading.show_xray = v @staticmethod def shading_cavity_get(): return get_view3d_space().shading.show_cavity @staticmethod def shading_cavity_set(v): get_view3d_space().shading.show_cavity = v @staticmethod def shading_outline_get(): return get_view3d_space().shading.show_object_outline @staticmethod def shading_outline_set(v): get_view3d_space().shading.show_object_outline = v @staticmethod def shading_restore(): for k in ['type','light','matcap','colortype','color','backface','shadows','xray','cavity','outline']: BlenderSettings._storerestore.restore(f'shading {k}') @staticmethod def quadview_get(): return bool(get_view3d_space().region_quadviews) @staticmethod def quadview_toggle(): bpy.ops.screen.region_quadview({'area': get_view3d_area(), 'region': BlenderSettings._get_region(label='window')}) @staticmethod def quadview_set(v): if BlenderSettings.quadview_get() != v: BlenderSettings.quadview_toggle() @staticmethod def quadview_hide(): BlenderSettings.quadview_set(False) @staticmethod def quadview_show(): BlenderSettings.quadview_set(True) @staticmethod def viewaa_get(): return bpy.context.preferences.system.viewport_aa @staticmethod def viewaa_set(v): bpy.context.preferences.system.viewport_aa = v @staticmethod def viewaa_simplify(): BlenderSettings.viewaa_set('FXAA' if BlenderSettings.viewaa_get() != 'OFF' else 'OFF') @staticmethod def clip_distances_get(): spc = get_view3d_space() return (spc.clip_start, spc.clip_end) @staticmethod def clip_distances_set(v): spc = get_view3d_space() spc.clip_start, spc.clip_end = v ######################################### # Overlays @staticmethod def overlays_get(): return get_view3d_space().overlay.show_overlays @staticmethod def overlays_set(v): get_view3d_space().overlay.show_overlays = v @staticmethod def overlays_hide(): BlenderSettings.overlays_set(False) @staticmethod def overlays_show(): BlenderSettings.overlays_set(True) @staticmethod def overlays_restore(): BlenderSettings._storerestore.restore('overlays') ######################################### # Gizmo @staticmethod def gizmo_get(): # return bpy.context.space_data.show_gizmo spc = get_view3d_space() settings = { k:getattr(spc, k) for k in dir(spc) if k.startswith('show_gizmo') } # print('manipulator_settings:', settings) return settings @staticmethod def gizmo_set(v): # bpy.context.space_data.show_gizmo = v spc = get_view3d_space() if type(v) is bool: for k in dir(spc): # DO NOT CHANGE `show_gizmo` VALUE if not k.startswith('show_gizmo_'): continue setattr(spc, k, v) else: for k,v_ in v.items(): setattr(spc, k, v_) @staticmethod def gizmo_hide(): BlenderSettings.gizmo_set(False) @staticmethod def gizmo_show(): BlenderSettings.gizmo_set(True) ######################################### # StoreRestore instance @staticmethod def storerestore_init(*, init_storage=None, clear_callbacks=True): cls = BlenderSettings cls._storerestore = StoreRestore(init_storage=init_storage) cls._storerestore.bind_all([ # ('workspace', cls.workspace_get, cls.workspace_set), # ('scene', cls.scene_get, cls.scene_set), # IMPORTANT: visible must be _before_ selected, because object must be visible before it can be selected ('objects visible', cls.objects_visible_get, cls.objects_visible_set, cls.objects_visible_restore), ('objects selected', cls.objects_selected_get, cls.objects_selected_set, cls.objects_selected_restore), ('object active', cls.object_active_get, cls.object_active_set), ('scene scale', cls.scene_scale_get, cls.scene_scale_set), ('panels', cls.panels_get, cls.panels_set), ('shading type', cls.shading_type_get, cls.shading_type_set), ('shading light', cls.shading_light_get, cls.shading_light_set), ('shading matcap', cls.shading_matcap_get, cls.shading_matcap_set), ('shading colortype', cls.shading_colortype_get, cls.shading_colortype_set), ('shading color', cls.shading_color_get, cls.shading_color_set), ('shading backface', cls.shading_backface_get, cls.shading_backface_set), ('shading shadows', cls.shading_shadows_get, cls.shading_shadows_set), ('shading xray', cls.shading_xray_get, cls.shading_xray_set), ('shading cavity', cls.shading_cavity_get, cls.shading_cavity_set), ('shading outline', cls.shading_outline_get, cls.shading_outline_set), ('quadview', cls.quadview_get, cls.quadview_set), ('overlays', cls.overlays_get, cls.overlays_set), ('gizmo', cls.gizmo_get, cls.gizmo_set), ('viewaa', cls.viewaa_get, cls.viewaa_set), ('clip distances', cls.clip_distances_get, cls.clip_distances_set), ]) if clear_callbacks: cls._storerestore.clear_storage_change_callbacks() def __init__(self, **kwargs): self.storerestore_init(**kwargs) @staticmethod def init_storage(*args, **kwargs): BlenderSettings._storerestore.init_storage(*args, **kwargs) @staticmethod def restore_all(*args, **kwargs): BlenderSettings._storerestore.restore_all(*args, **kwargs) def workspace_duplicate(*, context=None, name=None, use=True): # unfortunately, there isn't an elegant way to get a newly created workspace. # but, each workspace has a unique name, so we can use their names to determine # which workspace is new. context = context or bpy.context cur_name = context.window.workspace.name prev_workspaces = {workspace.name for workspace in bpy.data.workspaces} bpy.ops.workspace.duplicate() new_workspace = next((workspace for workspace in bpy.data.workspaces if workspace.name not in prev_workspaces)) if name: new_workspace.name = name context.window.workspace = new_workspace if use else bpy.data.workspaces[cur_name] return new_workspace def scene_duplicate(*, context=None, type='LINK_COPY', name=None, use=True): # unfortunately, there isn't an elegant way to get a newly created scene. # but, each scene has a unique name, so we can use their names to determine # which scene is new. context = context or bpy.context cur_name = context.window.scene.name prev_scenes = {scene.name for scene in bpy.data.scenes} bpy.ops.scene.new(type=type) new_scene = next((scene for scene in bpy.data.scenes if scene.name not in prev_scenes)) if name: new_scene.name = name context.window.scene = new_scene if use else bpy.data.scenes[cur_name] return new_scene def create_simple_context(context=None): return { a: getattr(context or bpy.context, a) for a in ['area', 'space_data', 'window', 'screen', 'region'] } # class Temp: # _store = {} # @classmethod # def assign(cls, var, val): # frame = inspect.currentframe().f_back # f_globals, f_locals = frame.f_globals, frame.f_locals # if var not in cls._store: # cls._store[var] = { # 'val': eval(var, globals=f_globals, locals=f_locals), # 'globals': f_globals, # 'locals': f_locals, # } # exec(f'{var} = {val}', globals=f_globals, locals=f_locals) # @classmethod # def restore_all(cls): # for var, data in cls._store.items(): # exec(f'{var} = {data["val"]}', globals=data['globals'], locals=data['locals']) # cls._store.clear() # @classmethod # def restore(cls, var): # val, f_globals, f_locals = cls._store[var] # exec(f'{var} = {data["val"]}', globals=f_globals, locals=f_locals) # del cls._store[var] # @classmethod # def discard(cls, var): # if var in cls._store[var]: # del cls._store[var] # class TempBPYData: # ''' # wrapper for bpy.data that allows the changing of settings while # storing original settings for later restoration. # This is _different_ than pre-storing the settings, in that we don't # need to know beforehand what should be stored/restored. # ''' # _always_store = True # _store = {} # is_bpy_type = lambda o: any(isinstance(o, t) for t in { # bpy.types.bpy_func, # bpy.types.bpy_prop, # bpy.types.bpy_prop_array, # bpy.types.bpy_struct, # bpy.types.bpy_struct_meta_idprop, # }) # is_stop_type = lambda o: any(isinstance(o, t) for t in { # bpy.types.Object, # }) # class WKey: # @classmethod # def Attr(cls, key): return cls(key, True) # @classmethod # def Item(cls, key): return cls(key, False) # def __init__(self, key, is_attr): # self._key = key # self._is_attr = is_attr # def __str__(self): # return f'.{self._key}' if self._is_attr else f'[{self._key}]' # @property # def key(self): return self._key # @property # def is_attr(self): return self._is_attr # @property # def is_item(self): return not self._is_attr # def get(self, data): # return getattr(data, self._key) if self._is_attr else data[self._key] # def set(self, data, val): # if self._is_attr: setattr(data, self._key, val) # else: data[self._key] = val # class Walker: # def __init__(self, *keys, from_walker=None): # if from_walker: # assert from_walker.in_struct # full_keys = from_walker.keys # data = from_walker.data # path = from_walker.path # else: # full_keys = tuple() # data = bpy.data # path = 'bpy.data' # pre_data = None # for key in keys: # data, pre_data, path = key.get(data), data # path = f'{path}{key}' # full_keys += key # self.__dict__['_data'] = { # 'path': path, # 'keys': full_keys, # 'data': data, # 'prev': (pre_data, keys[-1]), # 'in_struct': TempBPYData.is_bpy_type(data), # 'is_stop': TempBPYData.is_stop_type(data), # } # def __repr__(self): return f'' # def __iter__(self): return iter(self.data) # def __call__(self, *args, **kwargs): return self.data(*args, **kwargs) # def __getattr__(self, key): return self.get(key, True) # def __setattr__(self, key, val): return self.set(key, True, val) # def __getitem__(self, key): return self.get(key, False) # def __setitem__(self, key, val): return self.set(key, False, val) # @classmethod # def unwrap(cls, val): return val.data if isinstance(val, cls) else val # @property # def path(self): return self.__dict__['_data']['path'] # @property # def keys(self): return self.__dict__['_data']['keys'] # @property # def data(self): return self.__dict__['_data']['data'] # @property # def prev(self): return self.__dict__['_data']['prev'] # @property # def in_struct(self): return self.__dict__['_data']['in_struct'] # @property # def is_struct(self): return self.__dict__['_data']['is_stop'] # def get(self, key, attr): # if self.is_stop: # return getattr(self.data, key) if attr else self.data[key] # wk = TempBPYData.WKey(key, attr) # w = TempBPYData.Walker(wk, from_walker=self) # return w if w.in_struct else w.data # def set(self, key, attr, val): # assert self.in_struct # TempBPYData.store(list(self.keys) + [(key, attr)]) # val = self.unwrap(val) # if attr: setattr(self.data, key, val) # else: self.data[key] = val # # def ignore(self) # @classmethod # def debug_print_store(cls): # print(f'TempBPYData.store = {{') # for keys_attrs, val in cls._store.items(): # print(f' {cls.keys_attrs_to_path(keys_attrs)}: {val}') # print(f'}}') # @classmethod # def get_from_keys_attrs(cls, keys_attrs): # data = bpy.data # for (key, attr) in keys_attrs: # data = cls.get_from_key(data, key) # return data # @classmethod # def get_from_key(cls, data, key): # return getattr(data, key[1:]) if key.startswith('.') else data[key][1:-1] # @classmethod # def set_from_key_attr(cls, data, key, attr, val): # if attr: setattr(data, key, val) # else: data[key] = val # @classmethod # def keys_to_path(cls, keys_attrs): # path = 'bpy.data' # for (key, attr) in keys_attrs: # path += f'.{key}' if attr else f'[{key}]' # return path # @classmethod # def store(cls, keys_attrs): # store_key = tuple(keys_attrs) # store_val = cls.get_from_keys_attrs(keys_attrs) # if cls._always_store or not cls.is_bpy_type(store_val): # # only remember previous values if keys points to a non bpy_type. # # an example of keys that point to a bpy_type that we would wish to assign # # bpy.data.window_managers[0].windows[0].view_layer.objects.active # cls._store.setdefault(store_key, store_val) # @classmethod # def clear(cls): # cls._store.clear() # @classmethod # def discard(cls, keys_attrs): # if type(keys_attrs) is cls.Walker: # keys_attrs = keys_attrs.key_attrs # if keys_attrs in cls._store: # del cls._store[keys_attrs] # @classmethod # def restore_all(cls, *, clear=True): # for (keys_attrs, val) in cls._store.items(): # data = bpy.data # for (key, attr) in keys_attrs[:-1]: # data = cls.get_from_key(data, key) # (key, attr) = keys_attrs[-1] # cls.set_from_key_attr(data, key, attr, val) # if clear: # cls.clear() # @classmethod # def is_bpy_type(cls, o): # if any(isinstance(o, t) for t in cls._stop_at_types): # return False # return any(isinstance(o, t) for t in cls._bpy_types) # @classmethod # def __getattr__(cls, key): return cls.Walker(cls.WKey(key, True)) # @classmethod # def __getitem__(cls, key): return cls.Walker(cls.WKey(key, False)) # def __init__(self): pass # bpy_data = TempBPYData() # if True: # win = bpy_data.window_managers[0].windows[0] # vlobjs = win.view_layer.objects # print(vlobjs.active) # vlobjs.active = bpy_data.objects['Suzanne'] # area = win.screen.areas[2] # space = area.spaces.active # space.show_region_ui = False # print([(o.name, o.select_get()) for o in vlobjs]) # TempBPYData.discard(space.show_region_ui) # TempBPYData.debug_print_store() # TempBPYData.restore_all() ########################################################### # Mode def mode_translate(mode): return { 'OBJECT': 'OBJECT', # for some reason, we must 'EDIT_MESH': 'EDIT', # translate bpy.context.mode 'SCULPT': 'SCULPT', # to something that 'PAINT_VERTEX': 'VERTEX_PAINT', # bpy.ops.object.mode_set() 'PAINT_WEIGHT': 'WEIGHT_PAINT', # accepts... 'PAINT_TEXTURE': 'TEXTURE_PAINT', }.get(mode, mode) # WHY DO YOU DO THIS, BLENDER!?!?!? def mode_set(mode): bpy.ops.object.mode_set(mode_translate(mode)) ############################################################# def index_of_area_space(area, space): return next(iter(i for (i,s) in enumerate(area.spaces) if s == space)) ############################################################# @add_cache('root', {}) def get_path_from_addon_root(*path_join): path_here = os.path.realpath(os.path.dirname(__file__)) path_addon_root = os.path.realpath(os.path.join(path_here, '..', '..')) return os.path.join(path_addon_root, *path_join) # fn_path = lambda filename: os.path.realpath(os.path.dirname(filename)) # path_here = fn_path(__file__) # if path_here not in get_path_from_addon_root.root: # import addon_utils # # NOTE: append '/' to end to prevent matching subfolders that have appended stuff # modules = [mod for mod in addon_utils.modules() if path_here.startswith(fn_path(mod.__file__) + '/')] # assert len(modules) == 1, f'Could not find root for add-on containing {path_here}: {modules}' # get_path_from_addon_root.root[path_here] = fn_path(modules[0].__file__) # return os.path.join(get_path_from_addon_root.root[path_here], *path_join) def get_path_from_addon_common(*path_join): path_here = os.path.dirname(__file__) return os.path.realpath(os.path.join(path_here, '..', *path_join)) def get_path_shortened_from_addon_root(path): path_addon = get_path_from_addon_root() path_addons = os.path.dirname(path_addon) path = os.path.realpath(path) assert path.startswith(path_addons), f'Unexpected start of path:\n {path=}\n {path_addons=}' return path[len(path_addons)+1:] # +1 to skip leading '/' ############################################################# class BlenderIcon: blender_icons = bpy.utils.previews.new() path_icons = get_path_from_addon_root() # default to add-on root @staticmethod def icon_id(file): if file not in BlenderIcon.blender_icons: BlenderIcon.blender_icons.load( file, os.path.join(BlenderIcon.path_icons, file), 'IMAGE', ) return BlenderIcon.blender_icons[file].icon_id ############################################################# class ModifierWrapper_Mirror: ''' normalize the mirror modifier API across 2.79 and 2.80 ''' @staticmethod def create_new(obj: bpy.types.Object): mod = ModifierWrapper_Mirror(obj, obj.modifiers.new("Mirror", type='MIRROR')) mod.set_defaults() return mod @staticmethod def get_from_object(obj): for mod in obj.modifiers: if mod.type != 'MIRROR': continue return ModifierWrapper_Mirror(obj, mod) return None def __init__(self, obj, modifier): self._reading = True self.obj = obj self.mod = modifier self.read() @property def x(self): return 'x' in self._symmetry @x.setter def x(self, v): if v: self._symmetry.add('x') else: self._symmetry.discard('x') self.write() @property def y(self): return 'y' in self._symmetry @y.setter def y(self, v): if v: self._symmetry.add('y') else: self._symmetry.discard('y') self.write() @property def z(self): return 'z' in self._symmetry @z.setter def z(self, v): if v: self._symmetry.add('z') else: self._symmetry.discard('z') self.write() @property def use_clip(self): return self.mod.use_clip @use_clip.setter def use_clip(self, v): self.mod.use_clip = v @property def xyz(self): return set(self._symmetry) @property def symmetry_threshold(self): return self._symmetry_threshold @symmetry_threshold.setter def symmetry_threshold(self, v): self._symmetry_threshold = max(0, float(v)) self.write() def enable_axis(self, axis): self._symmetry.add(axis) self.write() def disable_axis(self, axis): self._symmetry.discard(axis) self.write() def disable_all(self): self._symmetry.clear() self.write() def is_enabled_axis(self, axis): return axis in self._symmetry def set_defaults(self): self.mod.merge_threshold = 0.001 self.mod.show_expanded = False self.mod.show_on_cage = True self.mod.use_mirror_merge = True self.mod.show_viewport = True self.disable_all() @blender_version_wrapper('<', '2.80') def read(self): self._reading = True self._symmetry = set() if self.mod.use_x: self._symmetry.add('x') if self.mod.use_y: self._symmetry.add('y') if self.mod.use_z: self._symmetry.add('z') self._symmetry_threshold = self.mod.merge_threshold self.show_viewport = self.mod.show_viewport self._reading = False @blender_version_wrapper('>=', '2.80') def read(self): self._reading = True self._symmetry = set() if self.mod.use_axis[0]: self._symmetry.add('x') if self.mod.use_axis[1]: self._symmetry.add('y') if self.mod.use_axis[2]: self._symmetry.add('z') self._symmetry_threshold = self.mod.merge_threshold self.show_viewport = self.mod.show_viewport self._reading = False @blender_version_wrapper('<', '2.80') def write(self): if self._reading: return self.mod.use_x = self.x self.mod.use_y = self.y self.mod.use_z = self.z self.mod.merge_threshold = self._symmetry_threshold self.mod.show_viewport = self.show_viewport @blender_version_wrapper('>=', '2.80') def write(self): if self._reading: return self.mod.use_axis[0] = self.x self.mod.use_axis[1] = self.y self.mod.use_axis[2] = self.z self.mod.merge_threshold = self._symmetry_threshold self.mod.show_viewport = self.show_viewport ############################################################# # TODO: generalize these functions to be add_object, etc. def set_object_selection(o, sel): o.select_set(sel) def link_object(o): bpy.context.scene.collection.objects.link(o) def set_active_object(o): bpy.context.view_layer.objects.active = o # use this, because bpy.context might not Screen context! # see https://docs.blender.org/api/current/bpy.context.html def get_active_object(): return bpy.context.view_layer.objects.active def get_from_dict_or_object(o, k): return o[k] if type(o) is dict else getattr(o, k) def toggle_property(o, k): setattr(o, k, not getattr(o, k)) def toggle_screen_header(ctx): # print(f'Addon Common Warning: Cannot toggle header visibility (addon_common/common/blender.py: toggle_screen_header)') # print(f' Skipping while bug exists in Blender 4.0+, see: https://developer.blender.org/T93410') space = ctx['space_data'] if type(ctx) is dict else get_view3d_space(ctx) toggle_property(space, 'show_region_header') def toggle_screen_tool_header(ctx): space = ctx['space_data'] if type(ctx) is dict else get_view3d_space(ctx) toggle_property(space, 'show_region_tool_header') def toggle_screen_toolbar(ctx): space = ctx['space_data'] if type(ctx) is dict else get_view3d_space(ctx) toggle_property(space, 'show_region_toolbar') def toggle_screen_properties(ctx): space = ctx['space_data'] if type(ctx) is dict else get_view3d_space(ctx) toggle_property(space, 'show_region_ui') def toggle_screen_lastop(ctx): space = ctx['space_data'] if type(ctx) is dict else get_view3d_space(ctx) toggle_property(space, 'show_region_hud') # regions for 3D View: # 0 1 2 3 4 5 # 279: [ HEADER, TOOLS, TOOL_PROPS, UI, WINDOW ] # 280: [ HEADER, TOOLS, UI, HUD, WINDOW ] # 300: [ HEADER, TOOL_HEADER, TOOLS, UI, HUD, WINDOW ] # could hard code the indices, but these magic numbers might change. # will stick to magic (but also way more descriptive) types RegionData = namedtuple('RegionData', 'type fn_toggle') region_label_to_data = { 'header': RegionData('HEADER', toggle_screen_header), 'tool header': RegionData('TOOL_HEADER', toggle_screen_tool_header), 'tool shelf': RegionData('TOOLS', toggle_screen_toolbar), 'properties': RegionData('UI', toggle_screen_properties), 'hud': RegionData('HUD', toggle_screen_lastop), 'window': RegionData('WINDOW', None), } tagged_redraw_all = False tag_reasons = set() def tag_redraw_all(reason, only_tag=True): global tagged_redraw_all, tag_reasons if False: term_printer.sprint(f'tagging redraw all: already={1 if tagged_redraw_all else 0} only_tag={1 if only_tag else 0}') tagged_redraw_all = True tag_reasons.add(reason) if not only_tag: perform_redraw_all() def perform_redraw_all(only_area=None): global tagged_redraw_all, tag_reasons if not tagged_redraw_all: return if False: term_printer.sprint('Redrawing:', tag_reasons) tag_reasons.clear() tagged_redraw_all = False if only_area: only_area.tag_redraw() else: for wm in bpy.data.window_managers: for win in wm.windows: for ar in win.screen.areas: ar.tag_redraw() class BlenderPopupOperator: def __init__(self, idname, **kwargs): self.idname = idname self.kwargs = kwargs def draw(self, layout): layout.operator(self.idname, **self.kwargs) def show_blender_popup(message, *, title="Message", icon="INFO", wrap=80): ''' icons: NONE, QUESTION, ERROR, CANCEL, TRIA_RIGHT, TRIA_DOWN, TRIA_LEFT, TRIA_UP, ARROW_LEFTRIGHT, PLUS, DISCLOSURE_TRI_DOWN, DISCLOSURE_TRI_RIGHT, RADIOBUT_OFF, RADIOBUT_ON, MENU_PANEL, BLENDER, GRIP, DOT, COLLAPSEMENU, X, GO_LEFT, PLUG, UI, NODE, NODE_SEL, FULLSCREEN, SPLITSCREEN, RIGHTARROW_THIN, BORDERMOVE, VIEWZOOM, ZOOMIN, ZOOMOUT, ... see: https://git.blender.org/gitweb/gitweb.cgi/blender.git/blob/HEAD:/source/blender/editors/include/UI_icons.h ''' # noqa if not message: return if type(message) is list: lines = message else: lines = message.splitlines() if wrap > 0: nlines = [] for line in lines: if type(line) is str: spc = len(line) - len(line.lstrip()) while len(line) > wrap: i = line.rfind(' ',0,wrap) if i == -1: nlines += [line[:wrap]] line = line[wrap:] else: nlines += [line[:i]] line = line[i+1:] if line: line = ' '*spc + line nlines += [line] lines = nlines def draw(self,context): for line in lines: if type(line) is str: self.layout.label(text=line) elif type(line) is BlenderPopupOperator: line.draw(self.layout) bpy.context.window_manager.popup_menu(draw, title=title, icon=icon) return def show_error_message(message, **kwargs): kwargs.setdefault('title', 'Error') kwargs.setdefault('icon', 'ERROR') show_blender_popup(message, **kwargs) def get_text_block(name, create=True, error_on_fail=True): name = str(name) if name in bpy.data.texts: return bpy.data.texts[name] if not create: return None old = { t.name for t in bpy.data.texts } bpy.ops.text.new() new = { t.name for t in bpy.data.texts if t.name not in old } if error_on_fail: assert len(new) != 0, f'Could not create new text block ({name=})' assert len(new) == 1, f'Creating new text block added two text blocks? ({name=})' elif len(new) != 1: return None textblock = bpy.data.texts[new.pop()] textblock.name = name return textblock def show_blender_text(textblock_name, hide_header=True, goto_top=True): if textblock_name not in bpy.data.texts: # no textblock to show return txt = bpy.data.texts[textblock_name] if goto_top: txt.current_line_index = 0 txt.select_end_line_index = 0 # duplicate the current area then change it to a text editor area_dupli = bpy.ops.screen.area_dupli('INVOKE_DEFAULT') win = bpy.context.window_manager.windows[-1] area = win.screen.areas[-1] area.type = 'TEXT_EDITOR' # load the text file into the correct space for space in area.spaces: if space.type == 'TEXT_EDITOR': space.text = txt space.show_word_wrap = True space.show_syntax_highlight = False space.top = 0 if hide_header and area.regions[0].height != 1: # hide header toggle_screen_header({'window':win, 'region':area.regions[2], 'area':area, 'space_data':space}) def bversion(short=True): major,minor,rev = bpy.app.version bver_long = '%03d.%03d.%03d' % (major,minor,rev) bver_short = '%d.%02d' % (major, minor) return bver_short if short else bver_long