""" Generic Blender functions """ from pathlib import Path from fnmatch import fnmatch from typing import Any, List, Iterable, Optional, Tuple Datablock = Any import bpy from bpy_extras import asset_utils from asset_library.constants import RESOURCES_DIR #from asset_library.common.file_utils import no from os.path import abspath import subprocess class attr_set(): '''Receive a list of tuple [(data_path, "attribute" [, wanted value)] ] entering with-statement : Store existing values, assign wanted value (if any) exiting with-statement: Restore values to their old values ''' def __init__(self, attrib_list): self.store = [] # item = (prop, attr, [new_val]) for item in attrib_list: prop, attr = item[:2] self.store.append( (prop, attr, getattr(prop, attr)) ) if len(item) >= 3: try: setattr(prop, attr, item[2]) except TypeError: print(f'Cannot set attribute {attr} to {prop}') def __enter__(self): return self def __exit__(self, exc_type, exc_value, exc_traceback): for prop, attr, old_val in self.store: setattr(prop, attr, old_val) def get_overriden_col(ob, scene=None): scn = scene or bpy.context.scene cols = [c for c in bpy.data.collections if scn.user_of_id(c)] return next((c for c in cols if ob in c.all_objects[:] if all(not c.override_library for c in get_col_parents(c))), None) def get_view3d_persp(): windows = bpy.context.window_manager.windows view_3ds = [a for w in windows for a in w.screen.areas if a.type == 'VIEW_3D'] view_3d = next((a for a in view_3ds if a.spaces.active.region_3d.view_perspective == 'PERSP'), view_3ds[0]) return view_3d def biggest_asset_browser_area(screen: bpy.types.Screen) -> Optional[bpy.types.Area]: """Return the asset browser Area that's largest on screen. :param screen: context.window.screen :return: the Area, or None if no Asset Browser area exists. """ def area_sorting_key(area: bpy.types.Area) -> Tuple[bool, int]: """Return area size in pixels.""" return (area.width * area.height) areas = list(suitable_areas(screen)) if not areas: return None return max(areas, key=area_sorting_key) def suitable_areas(screen: bpy.types.Screen) -> Iterable[bpy.types.Area]: """Generator, yield Asset Browser areas.""" for area in screen.areas: space_data = area.spaces[0] if not asset_utils.SpaceAssetInfo.is_asset_browser(space_data): continue yield area def area_from_context(context: bpy.types.Context) -> Optional[bpy.types.Area]: """Return an Asset Browser suitable for the given category. Prefers the current Asset Browser if available, otherwise the biggest. """ space_data = context.space_data if asset_utils.SpaceAssetInfo.is_asset_browser(space_data): return context.area # Try the current screen first. browser_area = biggest_asset_browser_area(context.screen) if browser_area: return browser_area for win in context.window_manager.windows: if win.screen == context.screen: continue browser_area = biggest_asset_browser_area(win.screen) if browser_area: return browser_area return None def activate_asset( asset: bpy.types.Action, asset_browser: bpy.types.Area, *, deferred: bool ) -> None: """Select & focus the asset in the browser.""" space_data = asset_browser.spaces[0] assert asset_utils.SpaceAssetInfo.is_asset_browser(space_data) space_data.activate_asset_by_id(asset, deferred=deferred) def active_catalog_id(asset_browser: bpy.types.Area) -> str: """Return the ID of the catalog shown in the asset browser.""" return params(asset_browser).catalog_id def get_asset_space_params(asset_browser: bpy.types.Area) -> bpy.types.FileAssetSelectParams: """Return the asset browser parameters given its Area.""" space_data = asset_browser.spaces[0] assert asset_utils.SpaceAssetInfo.is_asset_browser(space_data) return space_data.params def refresh_asset_browsers(): for area in suitable_areas(bpy.context.screen): bpy.ops.asset.library_refresh({"area": area, 'region': area.regions[3]}) def tag_redraw(screen: bpy.types.Screen) -> None: """Tag all asset browsers for redrawing.""" for area in suitable_areas(screen): area.tag_redraw() # def get_blender_command(file=None, script=None, background=True, **args): # '''Return a Blender Command as a list to be used in a subprocess''' # cmd = [bpy.app.binary_path] # if file: # cmd += [str(file)] # if background: # cmd += ['--background'] # if script: # cmd += ['--python', str(script)] # if args: # cmd += ['--'] # for k, v in args.items(): # cmd += [f"--{k.replace('_', '-')}", str(v)] # return cmd def norm_value(value): if isinstance(value, (tuple, list)): values = [] for v in value: if not isinstance(v, str): v = json.dumps(v) values.append(v) return values if isinstance(value, Path): return str(value) if not isinstance(value, str): value = json.dumps(value) return value def norm_arg(arg_name, format=str.lower, prefix='--', separator='-'): arg_name = norm_str(arg_name, format=format, separator=separator) return prefix + arg_name def get_bl_cmd(blender=None, background=False, focus=True, blendfile=None, script=None, **kargs): cmd = [str(blender)] if blender else [bpy.app.binary_path] if background: cmd += ['--background'] if not focus and not background: cmd += ['--no-window-focus'] cmd += ['--window-geometry', '5000', '0', '10', '10'] cmd += ['--python-use-system-env'] if blendfile: cmd += [str(blendfile)] if script: cmd += ['--python', str(script)] if kargs: cmd += ['--'] for k, v in kargs.items(): k = norm_arg(k) v = norm_value(v) cmd += [k] if isinstance(v, (tuple, list)): cmd += v else: cmd += [v] return cmd def get_addon_prefs(): addon_name = __package__.split('.')[0] return bpy.context.preferences.addons[addon_name].preferences def thumbnail_blend_file(input_blend, output_img): input_blend = Path(input_blend).resolve() output_img = Path(output_img).resolve() print(f'Thumbnailing {input_blend} to {output_img}') blender_thumbnailer = Path(bpy.app.binary_path).parent / 'blender-thumbnailer' output_img.parent.mkdir(exist_ok=True, parents=True) subprocess.call([blender_thumbnailer, str(input_blend), str(output_img)]) success = output_img.exists() if not success: empty_preview = RESOURCES_DIR / 'empty_preview.png' shutil.copy(str(empty_preview), str(output_img)) return success def get_col_parents(col, root=None, cols=None): '''Return a list of parents collections of passed col root : Pass a collection to search in (recursive) else search in master collection ''' if cols is None: cols = [] if root == None: root = bpy.context.scene.collection for sub in root.children: if sub == col: cols.append(root) if len(sub.children): cols = get_col_parents(col, root=sub, cols=cols) return cols def get_overriden_col(ob, scene=None): '''Get the collection use for making the override''' scn = scene or bpy.context.scene cols = [c for c in bpy.data.collections if scn.user_of_id(c)] return next((c for c in cols if ob in c.all_objects[:] if all(not c.override_library for c in get_col_parents(c))), None) def load_assets_from(filepath: Path) -> List[Datablock]: if not has_assets(filepath): # Avoid loading any datablocks when there are none marked as asset. return [] # Append everything from the file. with bpy.data.libraries.load(str(filepath)) as ( data_from, data_to, ): for attr in dir(data_to): setattr(data_to, attr, getattr(data_from, attr)) # Iterate over the appended datablocks to find assets. def loaded_datablocks() -> Iterable[Datablock]: for attr in dir(data_to): datablocks = getattr(data_to, attr) for datablock in datablocks: yield datablock loaded_assets = [] for datablock in loaded_datablocks(): if not getattr(datablock, "asset_data", None): continue # Fake User is lost when appending from another file. datablock.use_fake_user = True loaded_assets.append(datablock) return loaded_assets def has_assets(filepath: Path) -> bool: with bpy.data.libraries.load(str(filepath), assets_only=True) as ( data_from, _, ): for attr in dir(data_from): data_names = getattr(data_from, attr) if data_names: return True return False def copy_frames(start, end, offset, path): for i in range (start, end): src = path.replace('####', f'{i:04d}') dst = src.replace(src.split('_')[-1].split('.')[0], f'{i+offset:04d}') shutil.copy2(src, dst) def split_path(path) : try : bone_name = path.split('["')[1].split('"]')[0] except : bone_name = None try : prop_name = path.split('["')[2].split('"]')[0] except : prop_name = path.split('.')[-1] return bone_name, prop_name def load_datablocks(src, names=None, type='objects', link=True, expr=None, assets_only=False) -> list: return_list = not isinstance(names, str) names = names or [] if not isinstance(names, (list, tuple)): names = [names] if isinstance(expr, str): pattern = expr expr = lambda x : fnmatch(x, pattern) with bpy.data.libraries.load(str(src), link=link,assets_only=assets_only) as (data_from, data_to): datablocks = getattr(data_from, type) if expr: names += [i for i in datablocks if expr(i)] elif not names: names = datablocks setattr(data_to, type, names) datablocks = getattr(data_to, type) if return_list: return datablocks elif datablocks: return datablocks[0] """ # --- Collection handling """ def col_as_asset(col, verbose=False): if col is None: return if verbose: print('linking:', col.name) pcol = bpy.data.collections.new(col.name) bpy.context.scene.collection.children.link(pcol) pcol.children.link(col) pcol.asset_mark() return pcol def load_col(filepath, name, link=True, override=True, rig_pattern=None, context=None): '''Link a collection by name from a file and override if has armature''' # with bpy.data.libraries.load(filepath, link=link) as (data_from, data_to): # data_to.collections = [c for c in data_from.collections if c == name] # if not data_to.collections: # return # return data_to.collections[0] context = context or bpy.context col = load_datablocks(filepath, name, link=link, type='collections') ## create instance object inst = bpy.data.objects.new(col.name, None) inst.instance_collection = col inst.instance_type = 'COLLECTION' context.scene.collection.objects.link(inst) # make active inst.select_set(True) context.view_layer.objects.active = inst ## simple object (no armatures) if not link or not override: return inst if not next((o for o in col.all_objects if o.type == 'ARMATURE'), None): return inst ## Create the override # Search parent_cols = inst.users_collection child_cols = [child for pcol in parent_cols for child in pcol.children] params = {'active_object': inst, 'selected_objects': [inst]} try: bpy.ops.object.make_override_library(params) ## check which collection is new in parents collection asset_col = next((c for pcol in parent_cols for c in pcol.children if c not in child_cols), None) if not asset_col: print('Overriden, but no collection found !!') return for ob in asset_col.all_objects: if ob.type != 'ARMATURE': continue if rig_pattern and not fnmatch(ob.name, rig_pattern): continue ob.hide_select = ob.hide_viewport = False ob.select_set(True) context.view_layer.objects.active = ob print(ob.name) return ob except Exception as e: print(f'Override failed on {col.name}') print(e) return inst def get_preview(asset_path='', asset_name=''): asset_preview_dir = Path(asset_path).parents[1] name = asset_name.lower() return next((f for f in asset_preview_dir.rglob('*') if f.stem.lower().endswith(name)), None) def get_object_libraries(ob): if not ob : return [] libraries = [ob.library, ob.data.library] if ob.type in ('MESH', 'CURVE'): libraries += [m.library for m in ob.data.materials if m] filepaths = [] for l in libraries: if not l or not l.filepath: continue absolute_filepath = abspath(bpy.path.abspath(l.filepath, library=l)) if absolute_filepath in filepaths: continue filepaths.append(absolute_filepath) return filepaths