diff --git a/action/operators.py b/action/operators.py index 3bbbac8..6554252 100644 --- a/action/operators.py +++ b/action/operators.py @@ -1032,7 +1032,7 @@ class ACTIONLIB_OT_store_anim_pose(Operator): modified=time.time_ns() ) - lib.adapter.write_asset_description(asset_description, asset_path) + lib.adapter.write_description_file(asset_description, asset_path) # Restore action and cleanup ob.animation_data.action = current_action diff --git a/adapters/adapter.py b/adapters/adapter.py index c4e4a67..f9203fd 100644 --- a/adapters/adapter.py +++ b/adapters/adapter.py @@ -1,8 +1,9 @@ -from asset_library.common.functions import (read_catalog, write_catalog, norm_asset_datas, get_catalog_path) +from asset_library.common.functions import (read_catalog, write_catalog, norm_asset_datas) from asset_library.common.bl_utils import get_addon_prefs, load_datablocks from asset_library.common.file_utils import read_file, write_file from asset_library.common.template import Template +from asset_library.constants import (PREVIEW_ASSETS_SCRIPT, MODULE_DIR) from asset_library import (action, collection, file) @@ -18,6 +19,7 @@ import json import uuid import time from functools import partial +import subprocess class AssetLibraryAdapter(PropertyGroup): @@ -26,7 +28,7 @@ class AssetLibraryAdapter(PropertyGroup): name = "Base Adapter" #library = None - bundle_directory : StringProperty() + #bundle_directory : StringProperty() @property def library(self): @@ -37,29 +39,9 @@ class AssetLibraryAdapter(PropertyGroup): if lib.conform.adapter == self: return lib - @property - def library_path(self): - return self.library.library_path - - @property - def image_template(self): - return Template(self.library.image_template) - - @property - def video_template(self): - return Template(self.library.video_template) - - @property - def asset_description_template(self): - return Template(self.library.asset_description_template) - - @property - def data_type(self): - return self.library.data_type - - @property - def data_types(self): - return self.library.data_types + #@property + #def library_path(self): + # return self.library.library_path @property def is_conform(self): @@ -70,29 +52,70 @@ class AssetLibraryAdapter(PropertyGroup): if lib.conform.adapter == self: return True + @property + def target_directory(self): + if self.is_conform: + return self.library.conform.directory + + return self.library.bundle_dir + @property def blend_depth(self): if self.is_conform: return self.library.conform.blend_depth return self.library.blend_depth + + @property + def template_image(self): + return Template(self.library.conform.template_image) @property - def externalize_data(self): - return self.library.externalize_data + def template_video(self): + return Template(self.library.conform.template_video) @property - def catalog_path(self): - return self.library.catalog_path + def template_description(self): + return Template(self.library.conform.template_description) + - def get_catalog_path(self, filepath): - return get_catalog_path(filepath) + @property + def data_type(self): + return self.library.data_type + + @property + def data_types(self): + return self.library.data_types + + #@property + #def externalize_data(self): + # return self.library.externalize_data + + #@property + #def catalog_path(self): + # return self.library.catalog_path + + def get_catalog_path(self, directory=None): + directory = directory or self.target_directory + return Path(directory, 'blender_assets.cats.txt') @property def cache_file(self): - return Path(self.library_path) / f"blender_assets.{self.library.id}.json" + return Path(self.target_directory) / f"blender_assets.{self.library.id}.json" #return get_asset_datas_file(self.library_path) + @property + def diff_file(self): + return Path(bpy.app.tempdir, 'diff.json') + + @property + def preview_blend(self): + return MODULE_DIR / self.data_type.lower() / "preview.blend" + + @property + def preview_assets_file(self): + return Path(bpy.app.tempdir, "preview_assets_file.json") + @property def addon_prefs(self): return get_addon_prefs() @@ -116,6 +139,12 @@ class AssetLibraryAdapter(PropertyGroup): def norm_file_name(self, name): return name.replace(' ', '_') + def read_file(self, file): + return read_file(file) + + def write_file(self, file, data): + return write_file(file, data) + def copy_file(self, source, destination): src = Path(source) dst = Path(destination) @@ -185,6 +214,31 @@ class AssetLibraryAdapter(PropertyGroup): return asset_path + def get_template_path(self, template, name, asset_path, catalog): + + if template.startswith('.'): #the template is relative + template = Path(asset_path, template).as_posix() + + params = { + 'name': name, + 'asset_path': Path(asset_path), + 'catalog': catalog, + 'catalog_name': catalog.replace('/', '_'), + } + + return self.format_path(template, **params) + + def get_description_path(self, name, asset_path, catalog) -> Path: + """"Get the path of the json or yaml describing all assets data in one file""" + return self.get_template_path(self.library.conform.template_description, name, asset_path, catalog) + + def get_image_path(self, name, asset_path, catalog) -> Path: + return self.get_template_path(self.library.conform.template_image, name, asset_path, catalog) + + def get_video_path(self, name, asset_path, catalog) -> Path: + return self.get_template_path(self.library.conform.template_video, name, asset_path, catalog) + + ''' def get_path(self, type, name, asset_path, template=None) -> Path: if not template: template = getattr(self, f'{type}_template') @@ -193,27 +247,40 @@ class AssetLibraryAdapter(PropertyGroup): template = Template(template) filepath = Path(asset_path) - return (filepath / template.format(name=name, path=Path(asset_path))).resolve() + + params = { + 'bundle_dir': self.library.bundle_directory, + 'conform_dir': self.library.conform.directory, + 'rel_path': '', + 'catalog':'', + 'catalog_name':'', + 'name': name + } + + return self.format_path(template, params)#(filepath / template.format(name=name, path=Path(asset_path))).resolve() #def get_image_path(self, name, asset_path): # filepath = Path(asset_path) # image_name = self._get_file_name(name, asset_path) - # return (filepath / self.image_template.format(name=image_name)).resolve() + # return (filepath / self.template_image.format(name=image_name)).resolve() + def get_cache_image_path(self, name, catalog) -> Path: """"Get the the cache path of a image for asset without an externalized image""" + name = self.norm_file_name(name) return Path(self.library_path, '.previews', f"{catalog.replace('/', '_')}_{name}").with_suffix(('.png')) def get_cache_image(self, name, catalog): cache_image_path = self.get_cache_image_path(name, catalog) if cache_image_path.exists(): return cache_image_path + ''' #def get_video_path(self, name, asset_path): # filepath = Path(asset_path) # video_name = self._get_file_name(name, asset_path) - # return (filepath / self.video_template.format(name=video_name)).resolve() - + # return (filepath / self.template_video.format(name=video_name)).resolve() + ''' def get_image(self, name, asset_path): image_path = self.get_path('image', name, asset_path) if image_path.exists(): @@ -223,21 +290,19 @@ class AssetLibraryAdapter(PropertyGroup): video_path = self.get_path('video', name, asset_path) if video_path.exists(): return video_path + ''' - def get_asset_description_path(self, asset_path) -> Path: - """"Get the path of the json or yaml describing all assets data in onle file""" - filepath = Path(asset_path) - return (filepath / self.asset_description_template.format(name=filepath.stem)).resolve() - def read_asset_description(self, asset_path) -> dict: + + def read_asset_description_file(self, asset_path) -> dict: """Read the description file of the asset""" - asset_description_path = self.get_asset_description_path(asset_path) - return read_file(asset_description_path) + description_path = self.get_description_path(asset_path) + return self.read_file(description_path) - def write_asset_description(self, asset_data, asset_path) -> None: - asset_description_path = self.get_asset_description_path(asset_path) - return write_file(asset_description_path, asset_data) + def write_description_file(self, asset_data, asset_path) -> None: + description_path = self.get_description_path(asset_path) + return write_file(description_path, asset_data) def write_asset(self, asset, asset_path): bpy.data.libraries.write( @@ -247,27 +312,48 @@ class AssetLibraryAdapter(PropertyGroup): fake_user=True, compress=True ) - - def read_catalog(self, filepath=None): - """Read the catalog file of the library bundle path or of the specified filepath""" + def read_catalog(self, directory=None): + """Read the catalog file of the library target directory or of the specified directory""" + catalog_path = self.get_catalog_path(directory) - catalog_path = self.catalog_path - if filepath: - catalog_path = self.get_catalog_path(filepath) - return read_catalog(catalog_path) + cat_data = {} - def write_catalog(self, catalog_data, filepath=None): - """Write the catalog file in the library bundle path or of the specified filepath""" + for line in catalog_path.read_text(encoding="utf-8").split('\n'): + if line.startswith(('VERSION', '#')) or not line: + continue + + cat_id, cat_path, cat_name = line.split(':') + cat_data[cat_path] = {'id':cat_id, 'name':cat_name} - catalog_path = self.catalog_path - if filepath: - catalog_path = self.get_catalog_path(filepath) + return cat_data + + def write_catalog(self, catalog_data, directory=None): + """Write the catalog file in the library target directory or of the specified directory""" - return write_catalog(catalog_path, catalog_data) + catalog_path = self.get_catalog_path(directory) + + lines = ['VERSION 1', ''] + + # Add missing parents catalog + norm_data = {} + for cat_path, cat_data in catalog_data.items(): + norm_data[cat_path] = cat_data + for p in Path(cat_path).parents[:-1]: + if p in data or p in norm_data: + continue + + norm_data[p.as_posix()] = {'id': str(uuid.uuid4()), 'name': '-'.join(p.parts)} + + for cat_path, cat_data in sorted(norm_data.items()): + cat_name = cat_data['name'].replace('/', '-') + lines.append(f"{cat_data['id']}:{cat_path}:{cat_name}") + + print(f'Catalog writen at: {catalog_path}') + catalog_path.write_text('\n'.join(lines), encoding="utf-8") def read_cache(self): - return read_file(self.cache_file) + return self.read_file(self.cache_file) def norm_asset_datas(self, asset_file_datas): ''' Return a new flat list of asset data @@ -326,8 +412,148 @@ class AssetLibraryAdapter(PropertyGroup): return catalog_parts[:self.blend_depth] + #def transfert_preview(self, ) + + ''' + def generate_previews(self, assets, callback): + def _generate_previews(assets, callback, src_assets=None): + if src_assets: + src_assets = [] + + if bpy.app.is_job_running('RENDER_PREVIEW'): + print("Waiting for render...") + return 0.2 # waiting time + + while assets: # generate next preview + asset = assets.pop() + #print(f"Creating preview for world {world.name}...") + + asset_path = asset.asset_data['filepath'] + src_asset = self.load_datablocks(asset_path, names=asset.name, link=False, type=self.data_types) + if not src_asset: + #print(f'No asset named {asset.name} in {asset_path]}') + return + + src_assets.append(src_asset) + # # set image in the preview object's material + # obj = bpy.context.active_object + # image = world.node_tree.nodes['Environment Texture'].image + # obj.material_slots[0].material.node_tree.nodes['Image Texture'].image = image + if self.data_type == 'COLLECTION': + asset.children.link(src_asset) + + # start preview render + with bpy.context.temp_override(id=asset): + bpy.ops.ed.lib_id_generate_preview() + return 0.2 + + for asset in src_asset: + asset.user_clear() + bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) + + callback() + return None + + assets = assets.copy() + + # create preview images + bpy.app.timers.register( + functools.partial( + _generate_previews, + assets, + callback + ) + ) + ''' + def generate_preview(self, asset_description): + """Only generate preview when conforming a library""" + + #print('generate_preview', filepath, asset_names, data_type) + + scn = bpy.context.scene + #Creating the preview for collection, object or material + camera = scn.camera + vl = bpy.context.view_layer + + data_type = self.data_type #asset_description['data_type'] + asset_path = asset_description['filepath'] + asset_data_names = {} + for asset_data in asset_description['assets']: + + name = asset_data['name'] + catalog = asset_data['catalog'] + + image_path = self.get_image_path(name, asset_path, catalog) + if image_path.exists(): + continue + + #Store in a dict all asset_data that does not have preview + asset_data_names[name] = dict(asset_data, image_path=image_path) + + if not asset_data_names: + # No preview to generate + return + + asset_names = list(asset_data_names.keys()) + assets = self.load_datablocks(asset_path, names=asset_names, link=True, type=data_type) + + for asset in assets: + if not asset: + continue + + asset_data = asset_data_names[asset.name] + image_path = asset_data['image_path'] + if data_type == 'COLLECTION': + + bpy.ops.object.collection_instance_add(name=asset.name) + + bpy.ops.view3d.camera_to_view_selected() + instance = vl.objects.active + + #scn.collection.children.link(asset) + + scn.render.filepath = str(image_path) + + + + print(f'Render asset {asset.name} to {image_path}') + bpy.ops.render.render(write_still=True) + + #instance.user_clear() + asset.user_clear() + + bpy.data.objects.remove(instance) + + #bpy.ops.object.delete(use_global=False) + + #scn.collection.children.unlink(asset) + + bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) + + def generate_previews(self): + cache = self.fetch() + + #cache_diff.sort(key=lambda x :x['filepath']) + #blend_groups = groupby(cache_diff, key=lambda x :x['filepath']) + + #TODO Support all multiple data_type + for asset_description in cache: + self.generate_preview(asset_description) + + # filepath = asset_description['filepath'] + + # asset_datas = asset_description["assets"] + + # asset_datas.sort(key=lambda x :x.get('type', self.data_type)) + # data_type_groups = groupby(asset_datas, key=lambda x :x.get('type', self.data_type)) + + # for data_type, same_type_asset_datas in data_type_groups: + + # asset_names = [a['name'] for a in same_type_asset_datas] + # self.generate_preview(filepath, asset_names, data_type) + def set_asset_preview(self, asset, asset_data): - """Load an externalize image as preview for an asset""" + '''Load an externalize image as preview for an asset''' image_path = Path(asset_data['image']) if not image_path.is_absolute(): @@ -339,34 +565,52 @@ class AssetLibraryAdapter(PropertyGroup): bpy.ops.ed.lib_id_load_custom_preview( filepath=str(image_path) ) - return if asset.preview: - return - + return asset.preview + + return + #Creating the preview for collection, object or material src_asset = self.load_datablocks(asset_data['filepath'], names=asset_data['name'], link=False, type=self.data_types) if not src_asset: print(f'No asset named {asset_data["name"]} in {asset_data["filepath"]}') return - bpy.ops.ed.lib_id_generate_preview({"id": src_asset}) - #time.sleep(0.01) + if not self.data_type == 'COLLECTION': + print(f'Generate preview of type {self.data_type} not supported yet') + return - #Transfering pixels between previews - w, h = src_asset.preview.image_size - pixels = [0] * (w*h*4) - src_asset.preview.image_pixels_float.foreach_get(pixels) + # asset.children.link(src_asset) + # bpy.ops.ed.lib_id_generate_preview({"id": asset}) - asset.preview_ensure() - asset.preview.image_size = src_asset.preview.image_size - asset.preview.image_pixels_float.foreach_set(pixels) + # while bpy.app.is_job_running("RENDER_PREVIEW"): + # print(bpy.app.is_job_running("RENDER_PREVIEW")) + # time.sleep(0.2) + + # getattr(bpy.data, self.data_types).remove(src_asset) + # return asset.preview + #src_asset.user_clear() + #return src_asset + + #asset.children.unlink(src_asset) + #getattr(bpy.data, self.data_types).remove(src_asset) + # time.sleep(1) + + + # #Transfering pixels between previews + # w, h = src_asset.preview.image_size + # pixels = [0] * (w*h*4) + # src_asset.preview.image_pixels_float.foreach_get(pixels) + + # asset.preview_ensure() + # asset.preview.image_size = src_asset.preview.image_size + # asset.preview.image_pixels_float.foreach_set(pixels) #print('pixels transfered') - bpy.app.timers.register(partial(getattr(bpy.data, self.data_types).remove, src_asset), first_interval=1) - - #getattr(bpy.data, self.data_types).remove(src_asset) + #bpy.app.timers.register(partial(getattr(bpy.data, self.data_types).remove, src_asset), first_interval=1) + def set_asset_catalog(self, asset, asset_data, catalog_data): @@ -405,6 +649,10 @@ class AssetLibraryAdapter(PropertyGroup): continue asset.asset_data.tags.new(tag, skip_if_exists=True) + def set_asset_description(self, asset, asset_data): + """Set asset description base on provided data""" + asset.asset_data.description = asset_data.get('description', '') + def bundle(self, cache_diff=None): """Group all new assets in one or multiple blends for the asset browser""" @@ -412,7 +660,9 @@ class AssetLibraryAdapter(PropertyGroup): print(f'{self.data_type} is not supported yet') return - lib_path = self.library_path + target_dir = self.target_directory + + catalog_data = self.read_catalog() #TODO remove unused catalog write_cache = False @@ -423,11 +673,18 @@ class AssetLibraryAdapter(PropertyGroup): # Only write complete cache at the end write_cache = True + self.generate_previews() + elif isinstance(cache_diff, (Path, str)): cache_diff = json.loads(Path(cache_diff).read_text(encoding='utf-8')) + + + + if self.blend_depth == 0: - groups = [(cache_diff)] + raise Exception('Blender depth must be 1 at min') + #groups = [(cache_diff)] else: cache_diff.sort(key=self.group_key) groups = groupby(cache_diff, key=self.group_key) @@ -440,9 +697,10 @@ class AssetLibraryAdapter(PropertyGroup): return i = 0 + #assets_to_preview = [] for sub_path, asset_datas in groups: blend_name = sub_path[-1].replace(' ', '_').lower() - blend_path = Path(lib_path, *sub_path, blend_name).with_suffix('.blend') + blend_path = Path(target_dir, *sub_path, blend_name).with_suffix('.blend') if blend_path.exists(): print(f'Opening existing bundle blend: {blend_path}') @@ -471,9 +729,10 @@ class AssetLibraryAdapter(PropertyGroup): elif operation == 'ADD' or not asset: if asset: #raise Exception(f"Asset {asset_data['name']} Already in Blend") + print(f"Asset {asset_data['name']} Already in Blend") getattr(bpy.data, self.data_types).remove(asset) - #print(f"INFO: Add new asset: {asset_data['name']}") + print(f"INFO: Add new asset: {asset_data['name']}") asset = getattr(bpy.data, self.data_types).new(name=asset_data['name']) else: print(f'operation {operation} not supported should be in (ADD, REMOVE, MODIFIED)') @@ -482,26 +741,36 @@ class AssetLibraryAdapter(PropertyGroup): asset.asset_mark() self.set_asset_preview(asset, asset_data) + + #if not asset_preview: + # assets_to_preview.append((asset_data['filepath'], asset_data['name'], asset_data['data_type'])) #if self.externalize_data: # self.write_preview(preview, filepath) self.set_asset_catalog(asset, asset_data, catalog_data) self.set_asset_metadata(asset, asset_data) self.set_asset_tags(asset, asset_data) - asset.asset_data.description = asset_data.get('description', '') + self.set_asset_description(asset, asset_data) + i += 1 + #self.write_asset_preview_file() + + + print(f'Saving Blend to {blend_path}') - blend_path.parent.mkdir(exist_ok=True, parents=True) - bpy.ops.wm.save_as_mainfile(filepath=str(blend_path), compress=True) + #blend_path.parent.mkdir(exist_ok=True, parents=True) + + #bpy.ops.wm.save_as_mainfile(filepath=str(blend_path), compress=True) #if write_cache: # self.write_cache(cache) - #self.write_catalog(catalog_data) + self.write_catalog(catalog_data) + bpy.ops.wm.quit_blender() def norm_cache(self, cache): @@ -566,4 +835,11 @@ class AssetLibraryAdapter(PropertyGroup): layout.prop(self, k, text=bpy.path.display_name(k)) def format_path(self, template, **kargs): - return Template(template).format(self.to_dict(), **kargs).resolve() \ No newline at end of file + + params = dict(self.to_dict(), + bundle_dir=Path(self.library.bundle_directory), + conform_dir=Path(self.library.conform.directory), + **kargs + ) + + return Template(template).format(params).resolve() \ No newline at end of file diff --git a/adapters/kitsu.py b/adapters/kitsu.py index f07e558..1676b59 100644 --- a/adapters/kitsu.py +++ b/adapters/kitsu.py @@ -82,8 +82,8 @@ class KitsuLibrary(AssetLibraryAdapter): description=data['description'], tags=[], type=self.data_type, - image=str(self.image_template.format(name=asset_name)), - video=str(self.video_template.format(name=asset_name)), + image=str(self.template_image.format(name=asset_name)), + video=str(self.template_video.format(name=asset_name)), name=data['name']) ] ) @@ -98,17 +98,12 @@ class KitsuLibrary(AssetLibraryAdapter): def get_preview(self, asset_data): name = asset_data['name'] - preview = (f / image_template.format(name=name)).resolve() + preview = (f / template_image.format(name=name)).resolve() if not preview.exists(): preview_blend_file(f, preview) return preview - def conform(self, directory, templates): - """Split each assets per blend and externalize preview""" - - print(f'Conforming {self.library.name} to {directory}') - def fetch(self): """Gather in a list all assets found in the folder""" diff --git a/adapters/scan_folder.py b/adapters/scan_folder.py index 33afeef..3e2351d 100644 --- a/adapters/scan_folder.py +++ b/adapters/scan_folder.py @@ -55,8 +55,8 @@ class ScanFolderLibrary(AssetLibraryAdapter): metadata=dict(asset.asset_data), tags=asset.asset_data.tags.keys(), type=self.data_type, - image=str(self.image_template.format(name=asset_name)), - video=str(self.video_template.format(name=asset_name)), + image=str(self.template_image.format(name=asset_name)), + video=str(self.template_video.format(name=asset_name)), name=asset.name) ) @@ -178,7 +178,7 @@ class ScanFolderLibrary(AssetLibraryAdapter): asset.asset_mark() # Load external preview if exists - #image_template = Template(asset_data['preview']) + #template_image = Template(asset_data['preview']) image_path = Path(asset_data['image']) if not image_path.is_absolute(): image_path = Path(asset_data['filepath'], image_path) @@ -242,7 +242,7 @@ class ScanFolderLibrary(AssetLibraryAdapter): def get_preview(self, asset_data): name = asset_data['name'] - preview = (f / image_template.format(name=name)).resolve() + preview = (f / template_image.format(name=name)).resolve() if not preview.exists(): preview_blend_file(f, preview) @@ -264,8 +264,8 @@ class ScanFolderLibrary(AssetLibraryAdapter): catalog_ids = {v['id']: {'path': k, 'name': v['name']} for k,v in catalog_data.items()} directory = Path(directory).resolve() - image_template = templates.get('image') or self.image_template - video_template = templates.get('video') or self.video_template + template_image = templates.get('image') or self.template_image + template_video = templates.get('video') or self.template_video # Get list of all modifications for blend_file in self._find_blend_files(): @@ -295,12 +295,12 @@ class ScanFolderLibrary(AssetLibraryAdapter): asset_path = self.get_asset_path(name=asset.name, catalog=catalog_path, directory=directory) asset_description = self.get_asset_description(asset, catalog=catalog_path, modified=modified) - self.write_asset_description(asset_description, asset_path) + self.write_description_file(asset_description, asset_path) #Write blend file containing only one asset self.write_asset(asset=asset, asset_path=asset_path) # Copy image if source image found else write the asset preview - src_image_path = self.get_path('image', name=asset.name, asset_path=blend_file, template=image_template) + src_image_path = self.get_path('image', name=asset.name, asset_path=blend_file, template=template_image) dst_image_path = self.get_path('image', name=asset.name, asset_path=asset_path) if src_image_path.exists(): @@ -309,7 +309,7 @@ class ScanFolderLibrary(AssetLibraryAdapter): self.write_preview(asset.preview, dst_image_path) # Copy video if source video found - src_video_path = self.get_path('video', name=asset.name, asset_path=blend_file, template=video_template) + src_video_path = self.get_path('video', name=asset.name, asset_path=blend_file, template=template_video) #print('src_video_path', src_video_path) if src_video_path.exists(): @@ -364,7 +364,7 @@ class ScanFolderLibrary(AssetLibraryAdapter): if not field_data: raise Exception() - #asset_data = (blend_file / prefs.asset_description_template.format(name=name)).resolve() + #asset_data = (blend_file / prefs.template_description.format(name=name)).resolve() catalogs = [v for k,v in sorted(field_data.items()) if k.isdigit()] catalogs = [c.replace('_', ' ').title() for c in catalogs] @@ -387,7 +387,7 @@ class ScanFolderLibrary(AssetLibraryAdapter): continue #First Check if there is a asset_data .json - asset_description = self.read_asset_description(blend_file) + asset_description = self.read_asset_description_file(blend_file) if not asset_description: # Scan the blend file for assets inside and write a custom asset description for info found diff --git a/collection/preview.blend b/collection/preview.blend new file mode 100644 index 0000000..2ca4d64 Binary files /dev/null and b/collection/preview.blend differ diff --git a/common/bl_utils.py b/common/bl_utils.py index edac2c5..5fa190b 100644 --- a/common/bl_utils.py +++ b/common/bl_utils.py @@ -332,6 +332,9 @@ def load_datablocks(src, names=None, type='objects', link=True, expr=None) -> li return_list = not isinstance(names, str) names = names or [] + if type.isupper(): + type = f'{type.lower()}s' + if not isinstance(names, (list, tuple)): names = [names] diff --git a/common/file_utils.py b/common/file_utils.py index e0e93b5..3286202 100644 --- a/common/file_utils.py +++ b/common/file_utils.py @@ -13,6 +13,17 @@ import importlib import sys import shutil +import contextlib + +@contextlib.contextmanager +def cd(path): + """Changes working directory and returns to previous on exit.""" + prev_cwd = Path.cwd() + os.chdir(path) + try: + yield + finally: + os.chdir(prev_cwd) def install_module(module_name, package_name=None): '''Install a python module with pip or return it if already installed''' diff --git a/common/functions.py b/common/functions.py index 952cdeb..2163b41 100644 --- a/common/functions.py +++ b/common/functions.py @@ -181,6 +181,7 @@ def get_asset_source(replace_local=False): return source_path +''' def get_catalog_path(filepath=None): filepath = filepath or bpy.data.filepath filepath = Path(filepath) @@ -195,7 +196,7 @@ def get_catalog_path(filepath=None): catalog.touch(exist_ok=False) return catalog - +''' # def read_catalog(path, key='path'): # cat_data = {} @@ -219,7 +220,7 @@ def get_catalog_path(filepath=None): # cat_data[cat_name] = {'id':cat_id, 'path':cat_path} # return cat_data - +""" def read_catalog(path): cat_data = {} @@ -299,6 +300,7 @@ def create_catalog_file(json_path : str|Path, keep_existing_category : bool = Tr print(f'Catalog saved at: {catalog_path}') return +""" def clear_env_libraries(): print('clear_env_libraries') diff --git a/common/template.py b/common/template.py index a7d9230..f57065f 100644 --- a/common/template.py +++ b/common/template.py @@ -54,14 +54,12 @@ class Template: def format(self, data=None, **kargs): - #print('format', self.template, data, kargs) - data = {**(data or {}), **kargs} try: path = self.template.format(**data) - except KeyError: - print(f'Cannot format {self.template} with {data}') + except KeyError as e: + print(f'Cannot format {self.template} with {data}, field {e} is missing') return path = os.path.expandvars(path) diff --git a/constants.py b/constants.py index f00f20e..df7b508 100644 --- a/constants.py +++ b/constants.py @@ -13,4 +13,5 @@ ASSETLIB_FILENAME = "blender_assets.libs.json" MODULE_DIR = Path(__file__).parent RESOURCES_DIR = MODULE_DIR / 'resources' ADAPTER_DIR = MODULE_DIR / 'adapters' -ADAPTERS = [] \ No newline at end of file +ADAPTERS = [] +PREVIEW_ASSETS_SCRIPT = MODULE_DIR / 'common' / 'preview_assets.py' diff --git a/file/bundle.py b/file/bundle.py index f4e2da7..2e5e440 100644 --- a/file/bundle.py +++ b/file/bundle.py @@ -17,7 +17,7 @@ command, write_catalog) @command -def bundle_library(source_directory, bundle_directory, asset_description_template, thumbnail_template, +def bundle_library(source_directory, bundle_directory, template_description, thumbnail_template, template=None, data_file=None): field_pattern = r'{(\w+)}' @@ -38,7 +38,7 @@ def bundle_library(source_directory, bundle_directory, asset_description_templat name = field_data.get('name', f.stem) thumbnail = (f / thumbnail_template.format(name=name)).resolve() - asset_data = (f / asset_description_template.format(name=name)).resolve() + asset_data = (f / template_description.format(name=name)).resolve() catalogs = sorted([v for k,v in sorted(field_data.items()) if k.isdigit()]) catalogs = [c.replace('_', ' ').title() for c in catalogs] @@ -163,7 +163,7 @@ if __name__ == '__main__' : bundle_library( source_directory=args.source_directory, bundle_directory=args.bundle_directory, - asset_description_template=args.asset_description_template, + template_description=args.template_description, thumbnail_template=args.thumbnail_template, template=args.template, data_file=args.data_file) diff --git a/operators.py b/operators.py index 2fb20a1..62cf8b9 100644 --- a/operators.py +++ b/operators.py @@ -112,8 +112,8 @@ class ASSETLIB_OT_edit_data(Operator): new_video_path = lib.adapter.get_path('video', new_name, new_asset_path) self.old_video_path.rename(new_video_path) - if self.old_asset_description_path.exists(): - self.old_asset_description_path.unlink() + if self.old_description_path.exists(): + self.old_description_path.unlink() new_asset_description = lib.adapter.get_asset_description( asset=self.asset, @@ -121,7 +121,7 @@ class ASSETLIB_OT_edit_data(Operator): modified=time.time_ns() ) - lib.adapter.write_asset_description(new_asset_description, new_asset_path) + lib.adapter.write_description_file(new_asset_description, new_asset_path) if not list(self.old_asset_path.parent.iterdir()): self.old_asset_path.parent.rmdir() @@ -187,9 +187,9 @@ class ASSETLIB_OT_edit_data(Operator): self.old_image_path = lib.adapter.get_path('image', self.old_asset_name, self.old_asset_path) self.old_video_path = lib.adapter.get_path('video', self.old_asset_name, self.old_asset_path) - self.old_asset_description_path = lib.adapter.get_asset_description_path(self.old_asset_path) + self.old_description_path = lib.adapter.get_description_path(self.old_asset_path) - self.old_asset_description = lib.adapter.read_asset_description(self.old_asset_path) + self.old_asset_description = lib.adapter.read_asset_description_file(self.old_asset_path) self.old_asset_description = lib.adapter.norm_asset_datas([self.old_asset_description])[0] @@ -408,8 +408,8 @@ class ASSETLIB_OT_conform_library(Operator): bl_description = "Split each assets per blend and externalize preview" name : StringProperty() - image_template : StringProperty() - video_template : StringProperty() + template_image : StringProperty() + template_video : StringProperty() directory : StringProperty(subtype='DIR_PATH', name='Filepath') def execute(self, context: Context) -> Set[str]: @@ -419,13 +419,13 @@ class ASSETLIB_OT_conform_library(Operator): #lib.adapter.conform(self.directory) templates = {} - if self.image_template: - templates['image'] = self.image_template - if self.video_template: - templates['video'] = self.video_template + if self.template_image: + templates['image'] = self.template_image + if self.template_video: + templates['video'] = self.template_video - script_path = Path(gettempdir()) / 'bundle_library.py' + script_path = Path(bpy.app.tempdir) / 'bundle_library.py' script_code = dedent(f""" import bpy prefs = bpy.context.preferences.addons["asset_library"].preferences @@ -447,6 +447,58 @@ class ASSETLIB_OT_conform_library(Operator): return {'RUNNING_MODAL'} +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" + + diff : 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.adapter.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.conform.adapter.generate_previews() + """) + + 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"} @@ -553,6 +605,7 @@ classes = ( ASSETLIB_OT_add_user_library, ASSETLIB_OT_remove_user_library, ASSETLIB_OT_diff, + ASSETLIB_OT_generate_previews, ASSETLIB_OT_bundle_library, ASSETLIB_OT_clear_asset, ASSETLIB_OT_edit_data, diff --git a/prefs.py b/prefs.py index f806d1f..3dc003a 100644 --- a/prefs.py +++ b/prefs.py @@ -12,7 +12,7 @@ from asset_library.constants import (DATA_TYPES, DATA_TYPE_ITEMS, from asset_library.common.file_utils import import_module_from_path, norm_str from asset_library.common.bl_utils import get_addon_prefs -from asset_library.common.functions import get_catalog_path +#from asset_library.common.functions import get_catalog_path from pathlib import Path import importlib @@ -88,14 +88,16 @@ class ConformAssetLibrary(PropertyGroup): adapters : bpy.props.PointerProperty(type=AssetLibraryAdapters) adapter_name : EnumProperty(items=get_adapter_items) directory : StringProperty( - name="Destination Directory", + name="Target Directory", subtype='DIR_PATH', default='' ) - image_template : StringProperty() - video_template : StringProperty() - externalize_data: BoolProperty(default=False, name='Externalize Data') + template_image : StringProperty(default='', description='../{name}_image.png') + template_video : StringProperty(default='', description='../{name}_video.mov') + template_description : StringProperty(default='', description='../{name}_asset_description.json') + + #externalize_data: BoolProperty(default=False, name='Externalize Data') blend_depth: IntProperty(default=1, name='Blend Depth') @property @@ -228,23 +230,23 @@ class AssetLibrary(PropertyGroup): return self.name @property - def image_template(self): + def template_image(self): prefs = get_addon_prefs() - return prefs.image_template + return prefs.template_image @property - def video_template(self): + def template_video(self): prefs = get_addon_prefs() - return prefs.video_template + return prefs.template_video @property - def asset_description_template(self): + def template_description(self): prefs = get_addon_prefs() - return prefs.asset_description_template + return prefs.template_description - @property - def catalog_path(self): - return get_catalog_path(self.library_path) + #@property + #def catalog_path(self): + # return get_catalog_path(self.library_path) @property def options(self): @@ -376,7 +378,7 @@ class AssetLibrary(PropertyGroup): if not self.use: if all(not l.use for l in self.merge_libraries): self.clear_library_path() - return + return # Create the Asset Library Path if not lib: @@ -470,6 +472,10 @@ class AssetLibrary(PropertyGroup): op.name = self.name op.conform = True + op = subrow.operator('assetlib.generate_previews', text='', icon='SEQ_PREVIEW')#, icon='MOD_BUILD' + op.name = self.name + #op.conform = True + op = subrow.operator('assetlib.bundle', text='', icon='MOD_BUILD')#, icon='MOD_BUILD' op.name = self.name op.directory = self.conform.directory @@ -479,17 +485,22 @@ class AssetLibrary(PropertyGroup): #subrow.separator(factor=3) if self.expand_extra and self.conform.adapter: + col.separator() + self.conform.adapter.draw_prefs(col) + + col.separator() col.separator() #row = layout.row(align=True) #row.label(text='Conform Library') col.prop(self.conform, "directory") col.prop(self.conform, "blend_depth") - col.prop(self.conform, "externalize_data") - col.prop(self.conform, "image_template", text='Image Template') - col.prop(self.conform, "video_template", text='Video Template') + #col.prop(self.conform, "externalize_data") + subcol = col.column(align=True) + subcol.prop(self.conform, "template_description", text='Template Description', icon='COPY_ID') + subcol.prop(self.conform, "template_image", text='Template Image', icon='COPY_ID') + subcol.prop(self.conform, "template_video", text='Template Video', icon='COPY_ID') - col.separator() - self.conform.adapter.draw_prefs(col) + col.separator() @@ -651,10 +662,10 @@ class AssetLibraryPrefs(AddonPreferences): update=update_all_library_path ) - use_single_path : BoolProperty(default=True) - asset_description_template : StringProperty(default='../{name}_asset_description.json') - image_template : StringProperty(default='../{name}_image.png') - video_template : StringProperty(default='../{name}_video.mov') + #use_single_path : BoolProperty(default=True) + #template_description : StringProperty(default='../{name}_asset_description.json') + #template_image : StringProperty(default='../{name}_image.png') + #template_video : StringProperty(default='../{name}_video.mov') config_directory : StringProperty( name="Config Path", @@ -745,16 +756,16 @@ class AssetLibraryPrefs(AddonPreferences): col.separator() - col.prop(self, 'asset_description_template', text='Asset Description Template', icon='COPY_ID') + #col.prop(self, 'template_description', text='Asset Description Template', icon='COPY_ID') - col.separator() + #col.separator() - col.prop(self, 'image_template', text='Image Template', icon='COPY_ID') + #col.prop(self, 'template_image', text='Template Image', icon='COPY_ID') col.prop(self, 'image_player', text='Image Player') #icon='OUTLINER_OB_IMAGE' - col.separator() + #col.separator() - col.prop(self, 'video_template', text='Video Template', icon='COPY_ID') + #col.prop(self, 'template_video', text='Template Video', icon='COPY_ID') col.prop(self, 'video_player', text='Video Player') #icon='FILE_MOVIE' col.separator()