from typing import Set #import shutil from pathlib import Path import subprocess import importlib import time import json import bpy from bpy_extras import asset_utils from bpy.types import Context, Operator from bpy.props import ( BoolProperty, EnumProperty, StringProperty, IntProperty) #from asset_library.constants import (DATA_TYPES, DATA_TYPE_ITEMS, MODULE_DIR) import asset_library from asset_library.common.bl_utils import ( attr_set, get_addon_prefs, get_bl_cmd, get_view3d_persp, #suitable_areas, refresh_asset_browsers, load_datablocks) from asset_library.common.file_utils import open_blender_file, synchronize from asset_library.common.functions import get_active_library, asset_warning_callback from textwrap import dedent from tempfile import gettempdir import gpu from gpu_extras.batch import batch_for_shader import blf import bgl class ASSETLIB_OT_remove_assets(Operator): bl_idname = "assetlib.remove_assets" bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_label = 'Remove Assets' bl_description = 'Remove Selected Assets' @classmethod def poll(cls, context): if not asset_utils.SpaceAssetInfo.is_asset_browser(context.space_data): return False sp = context.space_data if sp.params.asset_library_ref == 'LOCAL': return False return True def execute(self, context: Context) -> Set[str]: asset = context.active_file lib = get_active_library() lib_type = lib.library_type asset_handle = context.asset_file_handle catalog_file = lib.library_type.read_catalog() catalog_ids = {v['id']: {'path': k, 'name': v['name']} for k,v in catalog_file.items()} catalog = catalog_ids[asset_handle.asset_data.catalog_id]['path'] asset_path = lib_type.format_path(asset.asset_data['filepath']) img_path = lib_type.get_image_path(name=asset_handle.name, catalog=catalog, filepath=asset_path) video_path = lib_type.get_video_path(name=asset_handle.name, catalog=catalog, filepath=asset_path) if asset_path and asset_path.exists(): asset_path.unlink() if img_path and img_path.exists(): img_path.unlink() if video_path and video_path.exists(): video_path.unlink() #open_blender_file(filepath) try: asset_path.parent.rmdir() except Exception:#Directory not empty pass bpy.ops.assetlib.bundle(name=lib.name, blocking=True) return {'FINISHED'} class ASSETLIB_OT_edit_data(Operator): bl_idname = "assetlib.edit_data" bl_label = "Edit Asset Data" bl_description = "Edit Current Asset Data" bl_options = {"REGISTER", "UNDO"} warning: StringProperty(name='') path: StringProperty(name='Path') catalog: StringProperty(name='Catalog', update=asset_warning_callback, options={'TEXTEDIT_UPDATE'}) name: StringProperty(name='Name', update=asset_warning_callback, options={'TEXTEDIT_UPDATE'}) tags: StringProperty(name='Tags', description='Tags need to separate with a comma (,)') description: StringProperty(name='Description') @classmethod def poll(cls, context): if not asset_utils.SpaceAssetInfo.is_asset_browser(context.space_data): return False return True def execute(self, context: Context) -> Set[str]: prefs = get_addon_prefs() lib = get_active_library() if lib.merge_libraries: lib = prefs.libraries[lib.store_library] new_name = lib.library_type.norm_file_name(self.name) new_asset_path = lib.library_type.get_asset_path(name=new_name, catalog=self.catalog) #asset_data = lib.library_type.get_asset_data(self.asset) asset_data = dict( tags=[t.strip() for t in self.tags.split(',') if t], description=self.description, ) #lib.library_type.set_asset_catalog(asset, asset_data, catalog_data) self.asset.name = self.name lib.library_type.set_asset_tags(self.asset, asset_data) lib.library_type.set_asset_info(self.asset, asset_data) self.old_asset_path.unlink() lib.library_type.write_asset(asset=self.asset, asset_path=new_asset_path) if self.old_image_path.exists(): new_img_path = lib.library_type.get_image_path(new_name, self.catalog, new_asset_path) self.old_image_path.rename(new_img_path) if self.old_video_path.exists(): new_video_path = lib.library_type.get_video_path(new_name, self.catalog, new_asset_path) self.old_video_path.rename(new_video_path) #if self.old_description_path.exists(): # self.old_description_path.unlink() try: self.old_asset_path.parent.rmdir() except Exception: #The folder is not empty pass diff_path = Path(bpy.app.tempdir, 'diff.json') diff = [dict(name=self.old_asset_name, catalog=self.old_catalog, filepath=str(self.old_asset_path), operation='REMOVE')] asset_data = lib.library_type.get_asset_data(self.asset) diff += [dict(asset_data, image=str(new_img_path), filepath=str(new_asset_path), type=lib.data_type, library_id=lib.id, catalog=self.catalog, operation='ADD' )] print(diff) diff_path.write_text(json.dumps(diff, indent=4), encoding='utf-8') bpy.ops.assetlib.bundle(name=lib.name, diff=str(diff_path), blocking=True) return {"FINISHED"} def draw(self, context): layout = self.layout layout.separator() layout.use_property_split = True lib = get_active_library() if lib.merge_libraries: layout.prop(lib, 'store_library', expand=False) layout.prop(self, "catalog", text="Catalog") layout.prop(self, "name", text="Name") layout.prop(self, 'tags') layout.prop(self, 'description') #layout.prop() layout.separator() col = layout.column() col.use_property_split = False #row.enabled = False if self.path: col.label(text=self.path) if self.warning: col.label(icon='ERROR', text=self.warning) def invoke(self, context, event): lib = get_active_library() active_lib = lib.library_type.get_active_asset_library() lib.store_library = active_lib.name asset_handle = context.asset_file_handle catalog_file = lib.library_type.read_catalog() catalog_ids = {v['id']: {'path': k, 'name': v['name']} for k,v in catalog_file.items()} #asset_handle = context.asset_file_handle self.old_asset_name = asset_handle.name self.old_asset_path = lib.library_type.get_active_asset_path() self.asset = load_datablocks(self.old_asset_path, self.old_asset_name, type=lib.data_types) if not self.asset: self.report({'ERROR'}, 'No asset found') self.name = self.old_asset_name self.description = asset_handle.asset_data.description tags = [t.strip() for t in self.asset.asset_data.tags.keys() if t] self.tags = ', '.join(tags) #asset_path self.old_catalog = catalog_ids[asset_handle.asset_data.catalog_id]['path'] self.catalog = self.old_catalog self.old_image_path = lib.library_type.get_image_path(name=self.name, catalog=self.catalog, filepath=self.old_asset_path) self.old_video_path = lib.library_type.get_video_path(name=self.name, catalog=self.catalog, filepath=self.old_asset_path) #self.old_description_path = lib.library_type.get_description_path(self.old_asset_path) #self.old_asset_info = lib.library_type.read_asset_info_file(self.old_asset_path) #self.old_asset_info = lib.library_type.norm_asset_datas([self.old_asset_info])[0] return context.window_manager.invoke_props_dialog(self, width=450) def cancel(self, context): print('Cancel Edit Data, removing the asset') lib = get_active_library() active_lib = lib.library_type.get_active_asset_library() getattr(bpy.data, active_lib.data_types).remove(self.asset) class ASSETLIB_OT_remove_user_library(Operator): bl_idname = "assetlib.remove_user_library" bl_options = {"REGISTER", "UNDO"} bl_label = 'Remove User Library' bl_description = 'Remove User Library' index : IntProperty(default=-1) def execute(self, context: Context) -> Set[str]: prefs = get_addon_prefs() prefs.user_libraries.remove(self.index) return {'FINISHED'} class ASSETLIB_OT_add_user_library(Operator): bl_idname = "assetlib.add_user_library" bl_options = {"REGISTER", "UNDO"} bl_label = 'Add User Library' bl_description = 'Add User Library' def execute(self, context: Context) -> Set[str]: prefs = get_addon_prefs() lib = prefs.user_libraries.add() lib.expand = True return {'FINISHED'} class ASSETLIB_OT_open_blend(Operator): bl_idname = "assetlib.open_blend" bl_options = {"REGISTER", "UNDO"} bl_label = 'Open Blender File' bl_description = 'Open blender file' #filepath : StringProperty(subtype='FILE_PATH') def execute(self, context: Context) -> Set[str]: #asset = context.active_file #prefs = get_addon_prefs() lib = get_active_library() #filepath = lib.library_type.format_path(asset.asset_data['filepath']) filepath = lib.library_type.get_active_asset_path() open_blender_file(filepath) return {'FINISHED'} class ASSETLIB_OT_set_paths(Operator): bl_idname = "assetlib.set_paths" bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_label = 'Set Paths' bl_description = 'Set Library Paths' name: StringProperty() all: BoolProperty(default=False) def execute(self, context: Context) -> Set[str]: prefs = get_addon_prefs() print('Set Paths') if self.all: libs = prefs.libraries else: libs = [prefs.libraries[self.name]] for lib in libs: lib.clear_library_path() lib.set_library_path() return {'FINISHED'} class ASSETLIB_OT_bundle_library(Operator): bl_idname = "assetlib.bundle" bl_options = {"INTERNAL"} bl_label = 'Bundle Library' bl_description = 'Bundle all matching asset found inside one blend' name : StringProperty() diff : StringProperty() blocking : BoolProperty(default=False) mode : EnumProperty(items=[(i.replace(' ', '_').upper(), i, '') for i in ('None', 'All', 'Auto Bundle')], default='NONE') directory : StringProperty(subtype='DIR_PATH') #conform : BoolProperty(default=False) #def refresh(self): # for area in suitable_areas(bpy.context.screen): # bpy.ops.asset.library_refresh({"area": area, 'region': area.regions[3]}) #space_data.activate_asset_by_id(asset, deferred=deferred) def execute(self, context: Context) -> Set[str]: prefs = get_addon_prefs() libs = [] if self.name: libs += [prefs.libraries[self.name]] if self.mode == 'ALL': libs += prefs.libraries.values() elif self.mode == 'AUTO_BUNDLE': libs += [l for l in prefs.libraries if l.auto_bundle] if not libs: return {"CANCELLED"} lib_datas = [l.to_dict() for l in libs] print(f'Bundle Libraries: {[l.name for l in libs]}') script_code = dedent(f""" import bpy prefs = bpy.context.preferences.addons["asset_library"].preferences for lib_data in {lib_datas}: lib = prefs.env_libraries.add() lib.set_dict(lib_data) lib.library_type.bundle(cache_diff='{self.diff}') bpy.ops.wm.quit_blender() """) script_path = Path(bpy.app.tempdir) / 'bundle_library.py' script_path.write_text(script_code) print(script_code) #raise Exception() cmd = get_bl_cmd(script=str(script_path), background=True) #print(cmd) if self.blocking: subprocess.call(cmd) bpy.app.timers.register(refresh_asset_browsers, first_interval=0.2) else: subprocess.Popen(cmd) return {'FINISHED'} class ASSETLIB_OT_reload_addon(Operator): bl_idname = "assetlib.reload_addon" bl_options = {"UNDO"} bl_label = 'Reload Asset Library Addon' bl_description = 'Reload The Asset Library Addon and the addapters' def execute(self, context: Context) -> Set[str]: print('Execute reload') asset_library.unregister() importlib.reload(asset_library) asset_library.register() return {'FINISHED'} class ASSETLIB_OT_diff(Operator): bl_idname = "assetlib.diff" bl_options = {"REGISTER", "UNDO"} bl_label = 'Synchronize' bl_description = 'Synchronize Action Lib to Local Directory' name : StringProperty() conform : BoolProperty(default=False) def execute(self, context: Context) -> Set[str]: prefs = get_addon_prefs() lib = prefs.libraries.get(self.name) lib.library_type.diff() return {'FINISHED'} ''' class ASSETLIB_OT_conform_library(Operator): bl_idname = "assetlib.conform_library" bl_options = {"REGISTER", "UNDO"} bl_label = "Conform Library" bl_description = "Split each assets per blend and externalize preview" name : StringProperty() template_image : StringProperty() template_video : StringProperty() directory : StringProperty(subtype='DIR_PATH', name='Filepath') def execute(self, context: Context) -> Set[str]: prefs = get_addon_prefs() lib = prefs.libraries.get(self.name) #lib.library_type.conform(self.directory) templates = {} if self.template_image: templates['image'] = self.template_image if self.template_video: templates['video'] = self.template_video script_path = Path(bpy.app.tempdir) / 'bundle_library.py' script_code = dedent(f""" import bpy prefs = bpy.context.preferences.addons["asset_library"].preferences lib = prefs.env_libraries.add() lib.set_dict({lib.to_dict()}) lib.library_type.conform(directory='{self.directory}', templates={templates}) """) script_path.write_text(script_code) cmd = get_bl_cmd(script=str(script_path), background=True) subprocess.Popen(cmd) return {'FINISHED'} def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} ''' class ASSETLIB_OT_make_custom_preview(Operator): bl_idname = "assetlib.make_custom_preview" bl_label = "Custom Preview" bl_description = "Set a camera to preview an asset" image_size : IntProperty(default=512) modal : BoolProperty(default=False) def modal(self, context, event): if event.type in {'ESC'}: # Cancel self.restore() return {'CANCELLED'} elif event.type in {'RET', 'NUMPAD_ENTER'}: # Cancel return self.execute(context) #return {'FINISHED'} return {'PASS_THROUGH'} def execute(self, context): prefs = get_addon_prefs() bpy.ops.render.opengl(write_still=True) img_path = context.scene.render.filepath #print('Load Image to previews') prefs.previews.load(Path(img_path).stem, img_path, 'IMAGE') #img = bpy.data.images.load(context.scene.render.filepath) #img.update() #img.preview_ensure() #Copy the image with a new name # render = bpy.data.images['Render Result'] # render_pixels = [0] * self.image_size * self.image_size * 4 # render.pixels.foreach_get(render_pixels) # img = bpy.data.images.new(name=img_name, width=self.image_size, height=self.image_size, is_data=True, alpha=True) # img.pixels.foreach_set(render_pixels) #img.scale(128, 128) #img.preview_ensure() # preview_size = render.size # pixels = [0] * preview_size[0] * preview_size[1] * 4 # render.pixels.foreach_get(pixels) # image.preview.image_size = preview_size # image.preview.image_pixels_float.foreach_set(pixels) self.restore() #self.is_running = False prefs.preview_modal = False return {"FINISHED"} def restore(self): print('RESTORE') try: bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') except: print('Failed remove handler') pass bpy.data.objects.remove(self.camera) self.attr_changed.restore() def draw_callback_px(self, context): if context.space_data != self._space_data: return dpi = context.preferences.system.dpi bg_color = (0.8, 0.1, 0.1, 0.5) font_color = (1, 1, 1, 1) text = f'Escape: Cancel Enter: Make Preview' font_id = 0 dim = blf.dimensions(font_id, text) #gpu.state.line_width_set(100) # bgl.glLineWidth(100) # self.shader_2d.bind() # self.shader_2d.uniform_float("color", bg_color) # self.screen_framing.draw(self.shader_2d) # # Reset # gpu.state.line_width_set(1) # -dim[0]/2, +dim[1]/2 + 5 # Display Text blf.color(font_id, *font_color) # unpack color blf.position(font_id, context.region.width/2 -dim[0]/2, dim[1]/2 + 5, 0) blf.size(font_id, 12, dpi) blf.draw(font_id, f'Escape: Cancel Enter: Make Preview') def get_image_name(self): prefs = get_addon_prefs() preview_names = [p for p in prefs.previews.keys()] preview_names.sort() index = 0 if preview_names: index = int(preview_names[-1][-2:]) + 1 return f'preview_{index:03d}' def invoke(self, context, event): prefs = get_addon_prefs() cam_data = bpy.data.cameras.new(name='Preview Camera') self.camera = bpy.data.objects.new(name='Preview Camera', object_data=cam_data) #view_3d = get_view3d_persp() scn = context.scene space = context.space_data matrix = space.region_3d.view_matrix.inverted() if space.region_3d.view_perspective == 'CAMERA': matrix = scn.camera.matrix_world self.camera.matrix_world = matrix img_name = self.get_image_name() img_path = Path(bpy.app.tempdir, img_name).with_suffix('.webp') self.attr_changed = attr_set([ (space.overlay, 'show_overlays', False), (space.region_3d, 'view_perspective', 'CAMERA'), (space.region_3d, 'view_camera_offset'), (space.region_3d, 'view_camera_zoom'), (space, 'lock_camera', True), (space, 'show_region_ui', False), (scn, 'camera', self.camera), (scn.render, 'resolution_percentage', 100), (scn.render, 'resolution_x', self.image_size), (scn.render, 'resolution_y', self.image_size), (scn.render, 'film_transparent', True), (scn.render.image_settings, 'file_format', 'WEBP'), (scn.render.image_settings, 'color_mode', 'RGBA'), #(scn.render.image_settings, 'color_depth', '8'), (scn.render, 'use_overwrite', True), (scn.render, 'filepath', str(img_path)), ]) bpy.ops.view3d.view_center_camera() space.region_3d.view_camera_zoom -= 6 space.region_3d.view_camera_offset[1] += 0.03 w, h = (context.region.width, context.region.height) self._space_data = context.space_data if self.modal: prefs.preview_modal = True self.shader_2d = gpu.shader.from_builtin('2D_UNIFORM_COLOR') self.screen_framing = batch_for_shader( self.shader_2d, 'LINE_LOOP', {"pos": [(0,0), (0,h), (w,h), (w,0)]}) self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_px, (context,), 'WINDOW', 'POST_PIXEL') context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} else: return self.execute(context) class ASSETLIB_OT_generate_previews(Operator): bl_idname = "assetlib.generate_previews" bl_options = {"REGISTER", "UNDO"} bl_label = "Generate Previews" bl_description = "Generate and write the image for assets" cache : StringProperty() preview_blend : StringProperty() name : StringProperty() blocking : BoolProperty(default=True) def execute(self, context: Context) -> Set[str]: prefs = get_addon_prefs() lib = prefs.libraries.get(self.name) # self.write_file(self.diff_file, self.diff) # preview_assets = [(a.asset_data['filepath'], self.data_types, a.name) for a in assets] # self.preview_assets_file.write_text(json.dumps(preview_assets), encoding='utf-8') # cmd = [ # bpy.app.binary_path, '-b', '--use-system-env', # '--python', str(PREVIEW_ASSETS_SCRIPT), '--', # '--preview-blend', str(self.preview_blend), # '--preview-assets-file', str(self.preview_assets_file) # ] # subprocess.call(cmd) preview_blend = self.preview_blend or lib.library_type.preview_blend if not preview_blend or not Path(preview_blend).exists(): preview_blend = MODULE_DIR / 'common' / 'preview.blend' script_path = Path(bpy.app.tempdir) / 'generate_previews.py' script_code = dedent(f""" import bpy prefs = bpy.context.preferences.addons["asset_library"].preferences lib = prefs.env_libraries.add() lib.set_dict({lib.to_dict()}) bpy.ops.wm.open_mainfile(filepath='{preview_blend}', load_ui=True) lib.library_type.generate_previews(cache='{self.cache}') """) script_path.write_text(script_code) cmd = get_bl_cmd(script=str(script_path), background=True) if self.blocking: subprocess.call(cmd) else: subprocess.Popen(cmd) return {'FINISHED'} class ASSETLIB_OT_play_preview(Operator): bl_idname = "assetlib.play_preview" bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_label = 'Play Preview' bl_description = 'Play Preview' @classmethod def poll(cls, context: Context) -> bool: if not context.active_file: return False if not asset_utils.SpaceAssetInfo.is_asset_browser(context.space_data): cls.poll_message_set("Current editor is not an asset browser") return False lib = get_active_library() if not lib: return False return True def execute(self, context: Context) -> Set[str]: asset = context.active_file prefs = get_addon_prefs() lib = get_active_library() #filepath = lib.library_type.format_path(asset.asset_data['filepath']) asset_path = lib.library_type.get_active_asset_path() asset_image = lib.library_type.get_image(asset.name, asset_path) asset_video = lib.library_type.get_video(asset.name, asset_path) if not asset_image and not asset_video: self.report({'ERROR'}, f'Preview for {asset.name} not found.') return {"CANCELLED"} if asset_video: self.report({'INFO'}, f'Video found. {asset_video}.') if prefs.video_player: subprocess.Popen([prefs.video_player, asset_video]) else: bpy.ops.wm.path_open(filepath=str(asset_video)) else: self.report({'INFO'}, f'Image found. {asset_image}.') if prefs.image_player: subprocess.Popen([prefs.image_player, asset_image]) else: bpy.ops.wm.path_open(filepath=str(asset_image)) return {"FINISHED"} class ASSETLIB_OT_synchronize(Operator): bl_idname = "assetlib.synchronize" bl_options = {"REGISTER", "UNDO"} bl_label = 'Synchronize' bl_description = 'Synchronize Action Lib to Local Directory' clean : BoolProperty(default=False) only_new : BoolProperty(default=False) only_recent : BoolProperty(default=False) name: StringProperty() all: BoolProperty(default=False) def execute(self, context: Context) -> Set[str]: print('Not yet Implemented, have to be replace by Bundle instead') return {'FINISHED'} prefs = get_addon_prefs() print('Synchronize') if self.all: libs = prefs.libraries else: libs = [prefs.libraries.get(self.name)] for lib in libs: if self.clean and Path(lib.path_local).exists(): pass print('To check first') #shutil.rmtree(path_local) if not lib.path_local: continue synchronize( src=lib.path, dst=lib.path_local, only_new=self.only_new, only_recent=self.only_recent ) return {'FINISHED'} classes = ( ASSETLIB_OT_play_preview, ASSETLIB_OT_open_blend, ASSETLIB_OT_set_paths, ASSETLIB_OT_synchronize, ASSETLIB_OT_add_user_library, ASSETLIB_OT_remove_user_library, ASSETLIB_OT_diff, ASSETLIB_OT_generate_previews, ASSETLIB_OT_bundle_library, ASSETLIB_OT_remove_assets, ASSETLIB_OT_edit_data, #ASSETLIB_OT_conform_library, ASSETLIB_OT_reload_addon, ASSETLIB_OT_make_custom_preview ) def register(): #bpy.types.UserAssetLibrary.is_env = False for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls)