start refacto
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
|
||||
from asset_library.plugins import plugin
|
||||
from asset_library.plugins import copy_folder
|
||||
from asset_library.plugins import scan_folder
|
||||
|
||||
if 'bpy' in locals():
|
||||
import importlib
|
||||
|
||||
importlib.reload(plugin)
|
||||
importlib.reload(copy_folder)
|
||||
importlib.reload(scan_folder)
|
||||
|
||||
import bpy
|
||||
|
||||
LibraryPlugin = plugin.LibraryPlugin
|
||||
CopyFolder = copy_folder.CopyFolder
|
||||
ScanFolder = scan_folder.ScanFolder
|
||||
@@ -0,0 +1,216 @@
|
||||
|
||||
"""
|
||||
Plugin for making an asset library of all blender file found in a folder
|
||||
"""
|
||||
|
||||
|
||||
from .scan_folder import ScanFolder
|
||||
from ..core.bl_utils import load_datablocks
|
||||
from ..core.template import Template
|
||||
|
||||
import bpy
|
||||
from bpy.props import (StringProperty, IntProperty, BoolProperty)
|
||||
import re
|
||||
from pathlib import Path
|
||||
from itertools import groupby
|
||||
import uuid
|
||||
import os
|
||||
import shutil
|
||||
import json
|
||||
import time
|
||||
from pprint import pprint
|
||||
|
||||
|
||||
class Conform(ScanFolder):
|
||||
|
||||
name = "Conform"
|
||||
source_directory : StringProperty(subtype='DIR_PATH')
|
||||
|
||||
target_template_file : StringProperty()
|
||||
target_template_info : StringProperty()
|
||||
target_template_image : StringProperty()
|
||||
target_template_video : StringProperty()
|
||||
|
||||
def draw_prefs(self, layout):
|
||||
layout.prop(self, "source_directory", text="Source : Directory")
|
||||
|
||||
col = layout.column(align=True)
|
||||
col.prop(self, "source_template_file", icon='COPY_ID', text='Template file')
|
||||
col.prop(self, "source_template_image", icon='COPY_ID', text='Template image')
|
||||
col.prop(self, "source_template_video", icon='COPY_ID', text='Template video')
|
||||
col.prop(self, "source_template_info", icon='COPY_ID', text='Template info')
|
||||
|
||||
col = layout.column(align=True)
|
||||
col.prop(self, "target_template_file", icon='COPY_ID', text='Target : Template file')
|
||||
col.prop(self, "target_template_image", icon='COPY_ID', text='Template image')
|
||||
col.prop(self, "target_template_video", icon='COPY_ID', text='Template video')
|
||||
col.prop(self, "target_template_info", icon='COPY_ID', text='Template info')
|
||||
|
||||
def get_asset_bundle_path(self, asset_data):
|
||||
"""Template file are relative"""
|
||||
|
||||
src_directory = Path(self.source_directory).resolve()
|
||||
src_template_file = Template(self.source_template_file)
|
||||
|
||||
asset_path = Path(asset_data['filepath']).as_posix()
|
||||
asset_path = self.format_path(asset_path)
|
||||
|
||||
rel_path = asset_path.relative_to(src_directory).as_posix()
|
||||
field_data = src_template_file.parse(rel_path)
|
||||
#field_data = {f"catalog_{k}": v for k, v in field_data.items()}
|
||||
|
||||
# Change the int in the template by string to allow format
|
||||
#target_template_file = re.sub(r'{(\d+)}', r'{cat\1}', self.target_template_file)
|
||||
|
||||
format_data = self.format_asset_data(asset_data)
|
||||
#format_data['asset_name'] = format_data['asset_name'].lower().replace(' ', '_')
|
||||
|
||||
path = Template(self.target_template_file).format(format_data, **field_data).with_suffix('.blend')
|
||||
path = Path(self.bundle_directory, path).resolve()
|
||||
|
||||
return path
|
||||
|
||||
def set_asset_preview(self, asset, asset_data):
|
||||
'''Load an externalize image as preview for an asset using the target template'''
|
||||
|
||||
image_template = self.target_template_image
|
||||
if not image_template:
|
||||
return
|
||||
|
||||
asset_path = self.get_asset_bundle_path(asset_data)
|
||||
image_path = self.find_path(image_template, asset_data, filepath=asset_path)
|
||||
|
||||
if image_path:
|
||||
with bpy.context.temp_override(id=asset):
|
||||
bpy.ops.ed.lib_id_load_custom_preview(
|
||||
filepath=str(image_path)
|
||||
)
|
||||
else:
|
||||
print(f'No image found for {image_template} on {asset.name}')
|
||||
|
||||
if asset.preview:
|
||||
return asset.preview
|
||||
|
||||
def generate_previews(self, cache_diff):
|
||||
|
||||
print('Generate previews...')
|
||||
|
||||
# if cache in (None, ''):
|
||||
# cache = self.fetch()
|
||||
# elif isinstance(cache, (Path, str)):
|
||||
# cache = self.read_cache(cache)
|
||||
|
||||
if isinstance(cache, (Path, str)):
|
||||
cache_diff = LibraryCacheDiff(cache_diff)
|
||||
|
||||
|
||||
|
||||
#TODO Support all multiple data_type
|
||||
for asset_info in cache:
|
||||
|
||||
if asset_info.get('type', self.data_type) == 'FILE':
|
||||
self.generate_blend_preview(asset_info)
|
||||
else:
|
||||
self.generate_asset_preview(asset_info)
|
||||
|
||||
def generate_asset_preview(self, asset_info):
|
||||
"""Only generate preview when conforming a library"""
|
||||
|
||||
#print('\ngenerate_preview', asset_info['filepath'])
|
||||
|
||||
scn = bpy.context.scene
|
||||
vl = bpy.context.view_layer
|
||||
#Creating the preview for collection, object or material
|
||||
#camera = scn.camera
|
||||
|
||||
data_type = self.data_type #asset_info['data_type']
|
||||
asset_path = self.format_path(asset_info['filepath'])
|
||||
|
||||
# Check if a source video exists and if so copying it in the new directory
|
||||
if self.source_template_video and self.target_template_video:
|
||||
for asset_data in asset_info['assets']:
|
||||
asset_data = dict(asset_data, filepath=asset_path)
|
||||
|
||||
dst_asset_path = self.get_asset_bundle_path(asset_data)
|
||||
dst_video_path = self.format_path(self.target_template_video, asset_data, filepath=dst_asset_path)
|
||||
if dst_video_path.exists():
|
||||
print(f'The dest video {dst_video_path} already exist')
|
||||
continue
|
||||
|
||||
src_video_path = self.find_path(self.source_template_video, asset_data)
|
||||
if src_video_path:
|
||||
print(f'Copy video from {src_video_path} to {dst_video_path}')
|
||||
self.copy_file(src_video_path, dst_video_path)
|
||||
|
||||
# Check if asset as a preview image or need it to be generated
|
||||
asset_data_names = {}
|
||||
|
||||
if self.target_template_image:
|
||||
for asset_data in asset_info['assets']:
|
||||
asset_data = dict(asset_data, filepath=asset_path)
|
||||
name = asset_data['name']
|
||||
dst_asset_path = self.get_asset_bundle_path(asset_data)
|
||||
|
||||
dst_image_path = self.format_path(self.target_template_image, asset_data, filepath=dst_asset_path)
|
||||
if dst_image_path.exists():
|
||||
print(f'The dest image {dst_image_path} already exist')
|
||||
continue
|
||||
|
||||
# Check if a source image exists and if so copying it in the new directory
|
||||
if self.source_template_image:
|
||||
src_image_path = self.find_path(self.source_template_image, asset_data)
|
||||
|
||||
if src_image_path:
|
||||
if src_image_path.suffix == dst_image_path.suffix:
|
||||
self.copy_file(src_image_path, dst_image_path)
|
||||
else:
|
||||
|
||||
print(src_image_path)
|
||||
self.save_image(src_image_path, dst_image_path, remove=True)
|
||||
|
||||
continue
|
||||
|
||||
#Store in a dict all asset_data that does not have preview
|
||||
asset_data_names[name] = dict(asset_data, image_path=dst_image_path)
|
||||
|
||||
|
||||
if not asset_data_names:# No preview to generate
|
||||
return
|
||||
|
||||
print('Making Preview for', list(asset_data_names.keys()))
|
||||
|
||||
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 asset.preview:
|
||||
print(f'Writing asset preview to {image_path}')
|
||||
self.write_preview(asset.preview, image_path)
|
||||
continue
|
||||
|
||||
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.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
"""
|
||||
Adapter for making an asset library of all blender file found in a folder
|
||||
"""
|
||||
|
||||
from os.path import expandvars
|
||||
|
||||
import bpy
|
||||
from bpy.props import StringProperty
|
||||
|
||||
from .library_plugin import LibraryPlugin
|
||||
from ..core.file_utils import copy_dir
|
||||
|
||||
|
||||
|
||||
|
||||
class CopyFolder(LibraryPlugin):
|
||||
"""Copy library folder from a server to a local disk for better performance"""
|
||||
|
||||
name = "Copy Folder"
|
||||
source_directory : StringProperty()
|
||||
|
||||
includes : StringProperty()
|
||||
excludes : StringProperty()
|
||||
|
||||
def bundle(self, cache_diff=None):
|
||||
src = expandvars(self.source_directory)
|
||||
dst = expandvars(self.bundle_directory)
|
||||
|
||||
includes = [inc.strip() for inc in self.includes.split(',')]
|
||||
excludes = [ex.strip() for ex in self.excludes.split(',')]
|
||||
|
||||
print(f'Copy Folder from {src} to {dst}...')
|
||||
copy_dir(
|
||||
src, dst, only_recent=True,
|
||||
excludes=excludes, includes=includes
|
||||
)
|
||||
|
||||
def filter_prop(self, prop):
|
||||
if prop in ('template_info', 'template_video', 'template_image', 'blend_depth'):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# def draw_prop(self, layout, prop):
|
||||
# if prop in ('template_info', 'template_video', 'template_image', 'blend_depth'):
|
||||
# return
|
||||
|
||||
# super().draw_prop(layout)
|
||||
@@ -0,0 +1,269 @@
|
||||
|
||||
"""
|
||||
Plugin for making an asset library of all blender file found in a folder
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from itertools import groupby
|
||||
import uuid
|
||||
import os
|
||||
import shutil
|
||||
import json
|
||||
import urllib3
|
||||
import traceback
|
||||
import time
|
||||
|
||||
import bpy
|
||||
from bpy.props import (StringProperty, IntProperty, BoolProperty)
|
||||
|
||||
from .library_plugin import LibraryPlugin
|
||||
from ..core.template import Template
|
||||
from ..core.file_utils import install_module
|
||||
|
||||
|
||||
class Kitsu(LibraryPlugin):
|
||||
|
||||
name = "Kitsu"
|
||||
template_name : StringProperty()
|
||||
template_file : StringProperty()
|
||||
source_directory : StringProperty(subtype='DIR_PATH')
|
||||
#blend_depth: IntProperty(default=1)
|
||||
source_template_image : StringProperty()
|
||||
target_template_image : StringProperty()
|
||||
|
||||
url: StringProperty()
|
||||
login: StringProperty()
|
||||
password: StringProperty(subtype='PASSWORD')
|
||||
project_name: StringProperty()
|
||||
|
||||
def connect(self, url=None, login=None, password=None):
|
||||
'''Connect to kitsu api using provided url, login and password'''
|
||||
|
||||
gazu = install_module('gazu')
|
||||
urllib3.disable_warnings()
|
||||
|
||||
if not self.url:
|
||||
print(f'Kitsu Url: {self.url} is empty')
|
||||
return
|
||||
|
||||
url = self.url
|
||||
if not url.endswith('/api'):
|
||||
url += '/api'
|
||||
|
||||
print(f'Info: Setting Host for kitsu {url}')
|
||||
gazu.client.set_host(url)
|
||||
|
||||
if not gazu.client.host_is_up():
|
||||
print('Error: Kitsu Host is down')
|
||||
|
||||
try:
|
||||
print(f'Info: Log in to kitsu as {self.login}')
|
||||
res = gazu.log_in(self.login, self.password)
|
||||
print(f'Info: Sucessfully login to Kitsu as {res["user"]["full_name"]}')
|
||||
return res['user']
|
||||
except Exception as e:
|
||||
print(f'Error: {traceback.format_exc()}')
|
||||
|
||||
def get_asset_path(self, name, catalog, directory=None):
|
||||
directory = directory or self.source_directory
|
||||
return Path(directory, self.get_asset_relative_path(name, catalog))
|
||||
|
||||
def get_asset_info(self, data, asset_path):
|
||||
|
||||
modified = time.time_ns()
|
||||
catalog = data['entity_type_name'].title()
|
||||
asset_path = self.prop_rel_path(asset_path, 'source_directory')
|
||||
#asset_name = self.norm_file_name(data['name'])
|
||||
|
||||
asset_info = dict(
|
||||
filepath=asset_path,
|
||||
modified=modified,
|
||||
library_id=self.library.id,
|
||||
assets=[dict(
|
||||
catalog=catalog,
|
||||
metadata=data.get('data', {}),
|
||||
description=data['description'],
|
||||
tags=[],
|
||||
type=self.data_type,
|
||||
#image=self.library.template_image,
|
||||
#video=self.library.template_video,
|
||||
name=data['name'])
|
||||
]
|
||||
)
|
||||
|
||||
return asset_info
|
||||
|
||||
# def bundle(self, cache_diff=None):
|
||||
# """Group all asset in one or multiple blends for the asset browser"""
|
||||
|
||||
# return super().bundle(cache_diff=cache_diff)
|
||||
|
||||
def set_asset_preview(self, asset, asset_data):
|
||||
'''Load an externalize image as preview for an asset using the source template'''
|
||||
|
||||
asset_path = self.format_path(Path(asset_data['filepath']).as_posix())
|
||||
|
||||
image_path = self.find_path(self.target_template_image, asset_data, filepath=asset_path)
|
||||
|
||||
if image_path:
|
||||
with bpy.context.temp_override(id=asset):
|
||||
bpy.ops.ed.lib_id_load_custom_preview(
|
||||
filepath=str(image_path)
|
||||
)
|
||||
else:
|
||||
print(f'No image found for {self.target_template_image} on {asset.name}')
|
||||
|
||||
if asset.preview:
|
||||
return asset.preview
|
||||
|
||||
|
||||
def generate_previews(self, cache=None):
|
||||
|
||||
print('Generate previews...')
|
||||
|
||||
if cache in (None, ''):
|
||||
cache = self.fetch()
|
||||
elif isinstance(cache, (Path, str)):
|
||||
cache = self.read_cache(cache)
|
||||
|
||||
#TODO Support all multiple data_type
|
||||
for asset_info in cache:
|
||||
|
||||
if asset_info.get('type', self.data_type) == 'FILE':
|
||||
self.generate_blend_preview(asset_info)
|
||||
else:
|
||||
self.generate_asset_preview(asset_info)
|
||||
|
||||
def generate_asset_preview(self, asset_info):
|
||||
|
||||
data_type = self.data_type
|
||||
scn = bpy.context.scene
|
||||
vl = bpy.context.view_layer
|
||||
|
||||
asset_path = self.format_path(asset_info['filepath'])
|
||||
|
||||
lens = 85
|
||||
|
||||
if not asset_path.exists():
|
||||
print(f'Blend file {asset_path} not exit')
|
||||
return
|
||||
|
||||
|
||||
asset_data_names = {}
|
||||
|
||||
# First check wich assets need a preview
|
||||
for asset_data in asset_info['assets']:
|
||||
name = asset_data['name']
|
||||
image_path = self.format_path(self.target_template_image, asset_data, filepath=asset_path)
|
||||
|
||||
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:
|
||||
print(f'All previews already existing for {asset_path}')
|
||||
return
|
||||
|
||||
#asset_names = [a['name'] for a in asset_info['assets']]
|
||||
asset_names = list(asset_data_names.keys())
|
||||
assets = self.load_datablocks(asset_path, names=asset_names, link=True, type=data_type)
|
||||
|
||||
print(asset_names)
|
||||
print(assets)
|
||||
|
||||
for asset in assets:
|
||||
if not asset:
|
||||
continue
|
||||
|
||||
print(f'Generate Preview for asset {asset.name}')
|
||||
|
||||
asset_data = asset_data_names[asset.name]
|
||||
|
||||
#print(self.target_template_image, asset_path)
|
||||
image_path = self.format_path(self.target_template_image, asset_data, filepath=asset_path)
|
||||
|
||||
# Force redo preview
|
||||
# if asset.preview:
|
||||
# print(f'Writing asset preview to {image_path}')
|
||||
# self.write_preview(asset.preview, image_path)
|
||||
# continue
|
||||
|
||||
if data_type == 'COLLECTION':
|
||||
|
||||
bpy.ops.object.collection_instance_add(name=asset.name)
|
||||
|
||||
scn.camera.data.lens = lens
|
||||
bpy.ops.view3d.camera_to_view_selected()
|
||||
scn.camera.data.lens -= 5
|
||||
|
||||
instance = vl.objects.active
|
||||
|
||||
#scn.collection.children.link(asset)
|
||||
|
||||
scn.render.filepath = str(image_path)
|
||||
scn.render.image_settings.file_format = self.format_from_ext(image_path.suffix)
|
||||
scn.render.image_settings.color_mode = 'RGBA'
|
||||
scn.render.image_settings.quality = 90
|
||||
|
||||
|
||||
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.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
|
||||
|
||||
def fetch(self):
|
||||
"""Gather in a list all assets found in the folder"""
|
||||
|
||||
print(f'Fetch Assets for {self.library.name}')
|
||||
|
||||
gazu = install_module('gazu')
|
||||
self.connect()
|
||||
|
||||
template_file = Template(self.template_file)
|
||||
template_name = Template(self.template_name)
|
||||
|
||||
project = gazu.client.fetch_first('projects', {'name': self.project_name})
|
||||
entity_types = gazu.client.fetch_all('entity-types')
|
||||
entity_types_ids = {e['id']: e['name'] for e in entity_types}
|
||||
|
||||
cache = self.read_cache()
|
||||
|
||||
for asset_data in gazu.asset.all_assets_for_project(project):
|
||||
asset_data['entity_type_name'] = entity_types_ids[asset_data.pop('entity_type_id')]
|
||||
asset_name = asset_data['name']
|
||||
|
||||
asset_field_data = dict(asset_name=asset_name, type=asset_data['entity_type_name'], source_directory=self.source_directory)
|
||||
|
||||
try:
|
||||
asset_field_data.update(template_name.parse(asset_name))
|
||||
except Exception:
|
||||
print(f'Warning: Could not parse {asset_name} with template {template_name}')
|
||||
|
||||
asset_path = template_file.find(asset_field_data)
|
||||
if not asset_path:
|
||||
print(f'Warning: Could not find file for {template_file.format(asset_field_data)}')
|
||||
continue
|
||||
|
||||
|
||||
asset_path = self.prop_rel_path(asset_path, 'source_directory')
|
||||
asset_cache_data = dict(
|
||||
catalog=asset_data['entity_type_name'].title(),
|
||||
metadata=asset_data.get('data', {}),
|
||||
description=asset_data['description'],
|
||||
tags=[],
|
||||
type=self.data_type,
|
||||
name=asset_data['name']
|
||||
)
|
||||
|
||||
cache.add_asset_cache(asset_cache_data, filepath=asset_path)
|
||||
|
||||
|
||||
return cache
|
||||
@@ -0,0 +1,773 @@
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import json
|
||||
import uuid
|
||||
import time
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from itertools import groupby
|
||||
from functools import partial
|
||||
from glob import glob
|
||||
from copy import deepcopy
|
||||
|
||||
import bpy
|
||||
from bpy_extras import asset_utils
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import StringProperty
|
||||
|
||||
#from asset_library.common.functions import (norm_asset_datas,)
|
||||
from ..core.bl_utils import get_addon_prefs, load_datablocks
|
||||
from ..core.file_utils import read_file, write_file
|
||||
from ..core.template import Template
|
||||
from ..constants import (MODULE_DIR, RESOURCES_DIR)
|
||||
|
||||
from ..data_type import (action, collection, file)
|
||||
#from asset_library.common.library_cache import LibraryCacheDiff
|
||||
|
||||
|
||||
|
||||
class LibraryPlugin(PropertyGroup):
|
||||
|
||||
#def __init__(self):
|
||||
#name = "Base Adapter"
|
||||
#library = None
|
||||
|
||||
@property
|
||||
def library(self):
|
||||
prefs = self.addon_prefs
|
||||
for lib in prefs.libraries:
|
||||
if lib.plugin == self:
|
||||
return lib
|
||||
|
||||
@property
|
||||
def bundle_directory(self):
|
||||
return self.library.library_path
|
||||
|
||||
@property
|
||||
def data_type(self):
|
||||
return self.library.data_type
|
||||
|
||||
@property
|
||||
def data_types(self):
|
||||
return self.library.data_types
|
||||
|
||||
# def get_catalog_path(self, directory=None):
|
||||
# directory = directory or self.bundle_directory
|
||||
# return Path(directory, 'blender_assets.cats.txt')
|
||||
|
||||
# @property
|
||||
# def cache_file(self):
|
||||
# return Path(self.bundle_directory) / f"blender_assets.{self.library.id}.json"
|
||||
|
||||
# @property
|
||||
# def tmp_cache_file(self):
|
||||
# return Path(bpy.app.tempdir) / f"blender_assets.{self.library.id}.json"
|
||||
|
||||
# @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()
|
||||
|
||||
@property
|
||||
def module_type(self):
|
||||
lib_type = self.library.data_type
|
||||
if lib_type == 'ACTION':
|
||||
return action
|
||||
elif lib_type == 'FILE':
|
||||
return file
|
||||
elif lib_type == 'COLLECTION':
|
||||
return collection
|
||||
|
||||
@property
|
||||
def format_data(self):
|
||||
"""Dict for formating template"""
|
||||
return dict(self.to_dict(), bundle_dir=self.library.bundle_dir, parent=self.library.parent)
|
||||
|
||||
def to_dict(self):
|
||||
return {p: getattr(self, p) for p in self.bl_rna.properties.keys() if p !='rna_type'}
|
||||
|
||||
def read_catalog(self):
|
||||
return self.library.read_catalog()
|
||||
|
||||
def read_cache(self, filepath=None):
|
||||
return self.library.read_cache(filepath=filepath)
|
||||
|
||||
def fetch(self):
|
||||
raise Exception('This method need to be define in the plugin')
|
||||
|
||||
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)
|
||||
|
||||
if not src.exists():
|
||||
print(f'Cannot copy file {src}: file not exist')
|
||||
return
|
||||
|
||||
dst.parent.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
if src == dst:
|
||||
print(f'Cannot copy file {src}: source and destination are the same')
|
||||
return
|
||||
|
||||
print(f'Copy file from {src} to {dst}')
|
||||
shutil.copy2(str(src), str(dst))
|
||||
|
||||
def load_datablocks(self, src, names=None, type='objects', link=True, expr=None, assets_only=False):
|
||||
"""Link or append a datablock from a blendfile"""
|
||||
|
||||
if type.isupper():
|
||||
type = f'{type.lower()}s'
|
||||
|
||||
return load_datablocks(src, names=names, type=type, link=link, expr=expr, assets_only=assets_only)
|
||||
|
||||
def get_asset_data(self, asset):
|
||||
"""Extract asset information on a datablock"""
|
||||
|
||||
return dict(
|
||||
name=asset.name,
|
||||
type=asset.bl_rna.name.upper(),
|
||||
author=asset.asset_data.author,
|
||||
tags=list(asset.asset_data.tags.keys()),
|
||||
metadata=dict(asset.asset_data),
|
||||
description=asset.asset_data.description,
|
||||
)
|
||||
|
||||
def get_asset_relative_path(self, name, catalog):
|
||||
'''Get a relative path for the asset'''
|
||||
name = self.norm_file_name(name)
|
||||
return Path(catalog, name, name).with_suffix('.blend')
|
||||
|
||||
def get_active_asset_library(self):
|
||||
prefs = get_addon_prefs()
|
||||
asset_handle = bpy.context.asset_file_handle
|
||||
|
||||
if not asset_handle:
|
||||
return self
|
||||
|
||||
lib = None
|
||||
if '.library_id' in asset_handle.asset_data:
|
||||
lib_id = asset_handle.asset_data['.library_id']
|
||||
lib = next((l for l in prefs.libraries if l.id == lib_id), None)
|
||||
|
||||
if not lib:
|
||||
print(f"No library found for id {lib_id}")
|
||||
|
||||
if not lib:
|
||||
lib = self
|
||||
|
||||
return lib
|
||||
|
||||
def get_active_asset_path(self):
|
||||
'''Get the full path of the active asset_handle from the asset brower'''
|
||||
prefs = get_addon_prefs()
|
||||
asset_handle = bpy.context.asset_file_handle
|
||||
|
||||
lib = self.get_active_asset_library()
|
||||
|
||||
if 'filepath' in asset_handle.asset_data:
|
||||
asset_path = asset_handle.asset_data['filepath']
|
||||
asset_path = lib.plugin.format_path(asset_path)
|
||||
else:
|
||||
asset_path = bpy.types.AssetHandle.get_full_library_path(
|
||||
asset_handle, bpy.context.asset_library_ref
|
||||
)
|
||||
|
||||
return asset_path
|
||||
|
||||
def generate_previews(self):
|
||||
raise Exception('Need to be defined in the plugin')
|
||||
|
||||
def get_image_path(self, name, catalog, filepath):
|
||||
raise Exception('Need to be defined in the plugin')
|
||||
|
||||
def get_video_path(self, name, catalog, filepath):
|
||||
raise Exception('Need to be defined in the plugin')
|
||||
|
||||
def new_asset(self, asset, asset_cache):
|
||||
raise Exception('Need to be defined in the plugin')
|
||||
|
||||
def remove_asset(self, asset, asset_cache):
|
||||
raise Exception('Need to be defined in the plugin')
|
||||
|
||||
def set_asset_preview(self, asset, asset_cache):
|
||||
raise Exception('Need to be defined in the plugin')
|
||||
|
||||
def format_asset_data(self, data):
|
||||
"""Get a dict for use in template fields"""
|
||||
return {
|
||||
'asset_name': data['name'],
|
||||
'asset_path': Path(data['filepath']),
|
||||
'catalog': data['catalog'],
|
||||
'catalog_name': data['catalog'].replace('/', '_'),
|
||||
}
|
||||
|
||||
def format_path(self, template, data={}, **kargs):
|
||||
if not template:
|
||||
return None
|
||||
|
||||
if data:
|
||||
data = self.format_asset_data(dict(data, **kargs))
|
||||
else:
|
||||
data = kargs
|
||||
|
||||
if template.startswith('.'): #the template is relative
|
||||
template = Path(data['asset_path'], template).as_posix()
|
||||
|
||||
params = dict(
|
||||
**data,
|
||||
**self.format_data,
|
||||
)
|
||||
|
||||
return Template(template).format(params).resolve()
|
||||
|
||||
def find_path(self, template, data, **kargs):
|
||||
path = self.format_path(template, data, **kargs)
|
||||
paths = glob(str(path))
|
||||
if paths:
|
||||
return Path(paths[0])
|
||||
|
||||
# def read_asset_info_file(self, asset_path) -> dict:
|
||||
# """Read the description file of the asset"""
|
||||
|
||||
# description_path = self.get_description_path(asset_path)
|
||||
# return self.read_file(description_path)
|
||||
|
||||
# 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):
|
||||
|
||||
Path(asset_path).parent.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
bpy.data.libraries.write(
|
||||
str(asset_path),
|
||||
{asset},
|
||||
path_remap="NONE",
|
||||
fake_user=True,
|
||||
compress=True
|
||||
)
|
||||
|
||||
# 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)
|
||||
|
||||
# if not catalog_path.exists():
|
||||
# return {}
|
||||
|
||||
# cat_data = {}
|
||||
|
||||
# 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}
|
||||
|
||||
# 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"""
|
||||
|
||||
# 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 cat_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, cache_path=None):
|
||||
# cache_path = cache_path or self.cache_file
|
||||
# print(f'Read cache from {cache_path}')
|
||||
# return self.read_file(cache_path)
|
||||
|
||||
# def write_cache(self, asset_infos, cache_path=None):
|
||||
# cache_path = cache_path or self.cache_file
|
||||
# print(f'cache file writen to {cache_path}')
|
||||
# return write_file(cache_path, list(asset_infos))
|
||||
|
||||
def prop_rel_path(self, path, prop):
|
||||
'''Get a filepath relative to a property of the plugin'''
|
||||
field_prop = '{%s}/'%prop
|
||||
|
||||
prop_value = getattr(self, prop)
|
||||
prop_value = Path(os.path.expandvars(prop_value)).resolve()
|
||||
|
||||
rel_path = Path(path).resolve().relative_to(prop_value).as_posix()
|
||||
|
||||
return field_prop + rel_path
|
||||
|
||||
def format_from_ext(self, ext):
|
||||
if ext.startswith('.'):
|
||||
ext = ext[1:]
|
||||
|
||||
file_format = ext.upper()
|
||||
|
||||
if file_format == 'JPG':
|
||||
file_format = 'JPEG'
|
||||
elif file_format == 'EXR':
|
||||
file_format = 'OPEN_EXR'
|
||||
|
||||
return file_format
|
||||
|
||||
def save_image(self, image, filepath, remove=False):
|
||||
filepath = Path(filepath)
|
||||
|
||||
if isinstance(image, (str, Path)):
|
||||
image = bpy.data.images.load(str(image))
|
||||
image.update()
|
||||
|
||||
image.filepath_raw = str(filepath)
|
||||
file_format = self.format_from_ext(filepath.suffix)
|
||||
|
||||
image.file_format = file_format
|
||||
image.save()
|
||||
|
||||
if remove:
|
||||
bpy.data.images.remove(image)
|
||||
else:
|
||||
return image
|
||||
|
||||
def write_preview(self, preview, filepath):
|
||||
if not preview or not filepath:
|
||||
return
|
||||
|
||||
filepath = Path(filepath)
|
||||
filepath.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
img_size = preview.image_size
|
||||
|
||||
px = [0] * img_size[0] * img_size[1] * 4
|
||||
preview.image_pixels_float.foreach_get(px)
|
||||
img = bpy.data.images.new(name=filepath.name, width=img_size[0], height=img_size[1], is_data=True, alpha=True)
|
||||
img.pixels.foreach_set(px)
|
||||
|
||||
self.save_image(img, filepath, remove=True)
|
||||
|
||||
|
||||
def draw_header(self, layout):
|
||||
"""Draw the header of the Asset Browser Window"""
|
||||
#layout.separator()
|
||||
|
||||
self.module_type.gui.draw_header(layout)
|
||||
|
||||
def draw_context_menu(self, layout):
|
||||
"""Draw the context menu of the Asset Browser Window"""
|
||||
self.module_type.gui.draw_context_menu(layout)
|
||||
|
||||
def generate_blend_preview(self, asset_info):
|
||||
asset_name = asset_info['name']
|
||||
catalog = asset_info['catalog']
|
||||
|
||||
asset_path = self.format_path(asset_info['filepath'])
|
||||
dst_image_path = self.get_image_path(asset_name, asset_path, catalog)
|
||||
|
||||
if dst_image_path.exists():
|
||||
return
|
||||
|
||||
# Check if a source image exists and if so copying it in the new directory
|
||||
src_image_path = asset_info.get('image')
|
||||
if src_image_path:
|
||||
src_image_path = self.get_template_path(src_image_path, asset_name, asset_path, catalog)
|
||||
if src_image_path and src_image_path.exists():
|
||||
self.copy_file(src_image_path, dst_image_path)
|
||||
return
|
||||
|
||||
print(f'Thumbnailing {asset_path} to {dst_image_path}')
|
||||
blender_thumbnailer = Path(bpy.app.binary_path).parent / 'blender-thumbnailer'
|
||||
|
||||
dst_image_path.parent.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
subprocess.call([blender_thumbnailer, str(asset_path), str(dst_image_path)])
|
||||
|
||||
success = dst_image_path.exists()
|
||||
|
||||
if not success:
|
||||
empty_preview = RESOURCES_DIR / 'empty_preview.png'
|
||||
self.copy_file(str(empty_preview), str(dst_image_path))
|
||||
|
||||
return success
|
||||
|
||||
'''
|
||||
def generate_asset_preview(self, asset_info):
|
||||
"""Only generate preview when conforming a library"""
|
||||
|
||||
#print('\ngenerate_preview', asset_info['filepath'])
|
||||
|
||||
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_info['data_type']
|
||||
asset_path = self.format_path(asset_info['filepath'])
|
||||
|
||||
# Check if a source video exists and if so copying it in the new directory
|
||||
if self.library.template_video:
|
||||
for asset_data in asset_info['assets']:
|
||||
dst_asset_path = self.get_asset_bundle_path(asset_data)
|
||||
dst_video_path = self.format_path(self.library.template_video, asset_data, filepath=dst_asset_path) #Template(src_video_path).find(asset_data, asset_path=dst_asset_path, **self.format_data)
|
||||
|
||||
if dst_video_path.exists():
|
||||
print(f'The dest video {dst_video_path} already exist')
|
||||
continue
|
||||
|
||||
src_video_template = asset_data.get('video')
|
||||
if not src_video_template:
|
||||
continue
|
||||
|
||||
src_video_path = self.find_path(src_video_template, asset_data, filepath=asset_path)#Template(src_video_path).find(asset_data, asset_path=dst_asset_path, **self.format_data)
|
||||
if src_video_path:
|
||||
print(f'Copy video from {src_video_path} to {dst_video_path}')
|
||||
self.copy_file(src_video_path, dst_video_path)
|
||||
|
||||
# Check if asset as a preview image or need it to be generated
|
||||
asset_data_names = {}
|
||||
|
||||
if self.library.template_image:
|
||||
for asset_data in asset_info['assets']:
|
||||
name = asset_data['name']
|
||||
dst_asset_path = self.get_asset_bundle_path(asset_data)
|
||||
|
||||
dst_image_path = self.format_path(self.library.template_image, asset_data, filepath=dst_asset_path)
|
||||
if dst_image_path.exists():
|
||||
print(f'The dest image {dst_image_path} already exist')
|
||||
continue
|
||||
|
||||
# Check if a source image exists and if so copying it in the new directory
|
||||
src_image_template = asset_data.get('image')
|
||||
if src_image_template:
|
||||
src_image_path = self.find_path(src_image_template, asset_data, filepath=asset_path)
|
||||
|
||||
if src_image_path:
|
||||
if src_image_path.suffix == dst_image_path.suffix:
|
||||
self.copy_file(src_image_path, dst_image_path)
|
||||
else:
|
||||
#img = bpy.data.images.load(str(src_image_path))
|
||||
self.save_image(src_image_path, dst_image_path, remove=True)
|
||||
|
||||
return
|
||||
|
||||
#Store in a dict all asset_data that does not have preview
|
||||
asset_data_names[name] = dict(asset_data, image_path=dst_image_path)
|
||||
|
||||
|
||||
if not asset_data_names:
|
||||
# No preview to generate
|
||||
return
|
||||
|
||||
print('Making Preview for', asset_data_names)
|
||||
|
||||
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 asset.preview:
|
||||
print(f'Writing asset preview to {image_path}')
|
||||
self.write_preview(asset.preview, image_path)
|
||||
continue
|
||||
|
||||
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.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
|
||||
'''
|
||||
|
||||
# def set_asset_catalog(self, asset, asset_data, catalog_data):
|
||||
# """Find the catalog if already exist or create it"""
|
||||
|
||||
|
||||
# catalog_name = asset_data['catalog']
|
||||
# catalog = catalog_data.get(catalog_name)
|
||||
|
||||
# catalog_item = self.catalog.add(asset_data['catalog'])
|
||||
# asset.asset_data.catalog_id = catalog_item.id
|
||||
|
||||
|
||||
# if not catalog:
|
||||
# catalog = {'id': str(uuid.uuid4()), 'name': catalog_name}
|
||||
# catalog_data[catalog_name] = catalog
|
||||
|
||||
# asset.asset_data.catalog_id = catalog['id']
|
||||
|
||||
def set_asset_metadata(self, asset, asset_cache):
|
||||
"""Create custom prop to an asset base on provided data"""
|
||||
for k, v in asset_cache.metadata.items():
|
||||
asset.asset_data[k] = v
|
||||
|
||||
def set_asset_tags(self, asset, asset_cache):
|
||||
"""Create asset tags base on provided data"""
|
||||
|
||||
if asset_cache.tags is not None:
|
||||
for tag in list(asset.asset_data.tags):
|
||||
asset.asset_data.tags.remove(tag)
|
||||
|
||||
for tag in asset_cache.tags:
|
||||
asset.asset_data.tags.new(tag, skip_if_exists=True)
|
||||
|
||||
def set_asset_info(self, asset, asset_cache):
|
||||
"""Set asset description base on provided data"""
|
||||
asset.asset_data.author = asset_cache.author
|
||||
asset.asset_data.description = asset_cache.description
|
||||
|
||||
def get_asset_bundle_path(self, asset_cache):
|
||||
"""Get the bundle path for that asset"""
|
||||
catalog_parts = asset_cache.catalog_item.parts
|
||||
blend_name = asset_cache.norm_name
|
||||
path_parts = catalog_parts[:self.library.blend_depth]
|
||||
|
||||
return Path(self.bundle_directory, *path_parts, blend_name, blend_name).with_suffix('.blend')
|
||||
|
||||
def bundle(self, cache_diff=None):
|
||||
"""Group all new assets in one or multiple blends for the asset browser"""
|
||||
|
||||
supported_types = ('FILE', 'ACTION', 'COLLECTION')
|
||||
supported_operations = ('ADD', 'REMOVE', 'MODIFY')
|
||||
|
||||
if self.data_type not in supported_types:
|
||||
print(f'{self.data_type} is not supported yet supported types are {supported_types}')
|
||||
return
|
||||
|
||||
catalog = self.read_catalog()
|
||||
cache = None
|
||||
|
||||
write_cache = False
|
||||
if not cache_diff:
|
||||
# Get list of all modifications
|
||||
cache = self.fetch()
|
||||
cache_diff = cache.diff()
|
||||
|
||||
# Write the cache in a temporary file for the generate preview script
|
||||
tmp_cache_file = cache.write(tmp=True)
|
||||
bpy.ops.assetlibrary.generate_previews(name=self.library.name, cache=str(tmp_cache_file))
|
||||
|
||||
elif isinstance(cache_diff, (Path, str)):
|
||||
cache_diff = LibraryCacheDiff(cache_diff).read()#json.loads(Path(cache_diff).read_text(encoding='utf-8'))
|
||||
|
||||
total_diffs = len(cache_diff)
|
||||
print(f'Total Diffs={total_diffs}')
|
||||
|
||||
if total_diffs == 0:
|
||||
print('No assets found')
|
||||
return
|
||||
|
||||
i = 0
|
||||
for bundle_path, asset_diffs in cache_diff.group_by(self.get_asset_bundle_path):
|
||||
if bundle_path.exists():
|
||||
print(f'Opening existing bundle blend: {bundle_path}')
|
||||
bpy.ops.wm.open_mainfile(filepath=str(bundle_path))
|
||||
else:
|
||||
print(f'Create new bundle blend to: {bundle_path}')
|
||||
bpy.ops.wm.read_homefile(use_empty=True)
|
||||
|
||||
for asset_diff in asset_diffs:
|
||||
if total_diffs <= 100 or i % int(total_diffs / 10) == 0:
|
||||
print(f'Progress: {int(i / total_diffs * 100)+1}')
|
||||
|
||||
operation = asset_diff.operation
|
||||
asset_cache = asset_diff.asset_cache
|
||||
asset = getattr(bpy.data, self.data_types).get(asset_cache.name)
|
||||
|
||||
if operation == 'REMOVE':
|
||||
if asset:
|
||||
getattr(bpy.data, self.data_types).remove(asset)
|
||||
else:
|
||||
print(f'ERROR : Remove Asset: {asset_cache.name} not found in {bundle_path}')
|
||||
continue
|
||||
|
||||
elif operation == 'MODIFY':
|
||||
if not asset:
|
||||
print(f'WARNING: Modifiy Asset: {asset_cache.name} not found in {bundle_path} it will be created')
|
||||
|
||||
if operation == 'ADD' or not asset:
|
||||
if asset:
|
||||
#raise Exception(f"Asset {asset_data['name']} Already in Blend")
|
||||
print(f"Asset {asset_cache.name} Already in Blend")
|
||||
getattr(bpy.data, self.data_types).remove(asset)
|
||||
|
||||
#print(f"INFO: Add new asset: {asset_data['name']}")
|
||||
asset = getattr(bpy.data, self.data_types).new(name=asset_cache.name)
|
||||
|
||||
|
||||
asset.asset_mark()
|
||||
|
||||
self.set_asset_preview(asset, asset_cache)
|
||||
|
||||
#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'])
|
||||
|
||||
asset.asset_data.catalog_id = catalog.add(asset_cache.catalog).id
|
||||
|
||||
self.set_asset_metadata(asset, asset_cache)
|
||||
self.set_asset_tags(asset, asset_cache)
|
||||
self.set_asset_info(asset, asset_cache)
|
||||
|
||||
|
||||
i += 1
|
||||
|
||||
#self.write_asset_preview_file()
|
||||
|
||||
print(f'Saving Blend to {bundle_path}')
|
||||
|
||||
bundle_path.parent.mkdir(exist_ok=True, parents=True)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(bundle_path), compress=True)
|
||||
|
||||
if write_cache:
|
||||
cache.write()
|
||||
|
||||
#self.write_catalog(catalog_data)
|
||||
catalog.write()
|
||||
|
||||
|
||||
bpy.ops.wm.quit_blender()
|
||||
|
||||
# def unflatten_cache(self, cache):
|
||||
# """ Return a new unflattten list of asset data
|
||||
# grouped by filepath"""
|
||||
|
||||
# new_cache = []
|
||||
|
||||
# cache = deepcopy(cache)
|
||||
|
||||
# cache.sort(key=lambda x : x['filepath'])
|
||||
# groups = groupby(cache, key=lambda x : x['filepath'])
|
||||
|
||||
# keys = ['filepath', 'modified', 'library_id']
|
||||
|
||||
# for _, asset_datas in groups:
|
||||
# asset_datas = list(asset_datas)
|
||||
|
||||
# #print(asset_datas[0])
|
||||
|
||||
# asset_info = {k:asset_datas[0][k] for k in keys}
|
||||
# asset_info['assets'] = [{k:v for k, v in a.items() if k not in keys+['operation']} for a in asset_datas]
|
||||
|
||||
# new_cache.append(asset_info)
|
||||
|
||||
# return new_cache
|
||||
|
||||
# def flatten_cache(self, cache):
|
||||
# """ Return a new flat list of asset data
|
||||
# the filepath keys are merge with the assets keys"""
|
||||
|
||||
# # If the cache has a wrong format
|
||||
# if not cache or not isinstance(cache[0], dict):
|
||||
# return []
|
||||
|
||||
# new_cache = []
|
||||
|
||||
# for asset_info in cache:
|
||||
# asset_info = asset_info.copy()
|
||||
# if 'assets' in asset_info:
|
||||
|
||||
# assets = asset_info.pop('assets')
|
||||
# for asset_data in assets:
|
||||
# new_cache.append({**asset_info, **asset_data})
|
||||
# else:
|
||||
# new_cache.append(asset_info)
|
||||
|
||||
# return new_cache
|
||||
|
||||
# def diff(self, asset_infos=None):
|
||||
# """Compare the library cache with it current state and return the new cache and the difference"""
|
||||
|
||||
# cache = self.read_cache()
|
||||
|
||||
# if cache is None:
|
||||
# print(f'Fetch The library {self.library.name} for the first time, might be long...')
|
||||
# cache = []
|
||||
|
||||
# asset_infos = asset_infos or self.fetch()
|
||||
|
||||
# cache = {f"{a['filepath']}/{a['name']}": a for a in self.flatten_cache(cache)}
|
||||
# new_cache = {f"{a['filepath']}/{a['name']}" : a for a in self.flatten_cache(asset_infos)}
|
||||
|
||||
# assets_added = [v for k, v in new_cache.items() if k not in cache]
|
||||
# assets_removed = [v for k, v in cache.items() if k not in new_cache]
|
||||
# assets_modified = [v for k, v in cache.items() if v not in assets_removed and v!= new_cache[k]]
|
||||
|
||||
# if assets_added:
|
||||
# print(f'{len(assets_added)} Assets Added \n{tuple(a["name"] for a in assets_added[:10])}\n')
|
||||
# if assets_removed:
|
||||
# print(f'{len(assets_removed)} Assets Removed \n{tuple(a["name"] for a in assets_removed[:10])}\n')
|
||||
# if assets_modified:
|
||||
# print(f'{len(assets_modified)} Assets Modified \n{tuple(a["name"] for a in assets_modified[:10])}\n')
|
||||
|
||||
# assets_added = [dict(a, operation='ADD') for a in assets_added]
|
||||
# assets_removed = [dict(a, operation='REMOVE') for a in assets_removed]
|
||||
# assets_modified = [dict(a, operation='MODIFY') for a in assets_modified]
|
||||
|
||||
# cache_diff = assets_added + assets_removed + assets_modified
|
||||
# if not cache_diff:
|
||||
# print('No change in the library')
|
||||
|
||||
# return list(new_cache.values()), cache_diff
|
||||
|
||||
def draw_prefs(self, layout):
|
||||
"""Draw the options in the addon preference for this plugin"""
|
||||
|
||||
annotations = self.__class__.__annotations__
|
||||
for k, v in annotations.items():
|
||||
layout.prop(self, k, text=bpy.path.display_name(k))
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
|
||||
"""
|
||||
Plugin for making an asset library of all blender file found in a folder
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
import shutil
|
||||
import json
|
||||
import requests
|
||||
import urllib3
|
||||
import traceback
|
||||
import time
|
||||
from itertools import groupby
|
||||
from pathlib import Path
|
||||
from pprint import pprint as pp
|
||||
|
||||
import bpy
|
||||
from bpy.props import (StringProperty, IntProperty, BoolProperty, EnumProperty)
|
||||
|
||||
from .library_plugin import LibraryPlugin
|
||||
from ..core.template import Template
|
||||
from ..core.file_utils import install_module
|
||||
|
||||
|
||||
REQ_HEADERS = requests.utils.default_headers()
|
||||
REQ_HEADERS.update({"User-Agent": "Blender: PH Assets"})
|
||||
|
||||
class PolyHaven(LibraryPlugin):
|
||||
|
||||
name = "Poly Haven"
|
||||
# template_name : StringProperty()
|
||||
# template_file : StringProperty()
|
||||
directory : StringProperty(subtype='DIR_PATH')
|
||||
asset_type : EnumProperty(items=[(i.replace(' ', '_').upper(), i, '') for i in ('HDRIs', 'Models', 'Textures')], default='HDRIS')
|
||||
main_category : StringProperty(
|
||||
default='artificial light, natural light, nature, studio, skies, urban'
|
||||
)
|
||||
secondary_category : StringProperty(
|
||||
default='high constrast, low constrast, medium constrast, midday, morning-afternoon, night, sunrise-sunset'
|
||||
)
|
||||
|
||||
#blend_depth: IntProperty(default=1)
|
||||
|
||||
# url: StringProperty()
|
||||
# login: StringProperty()
|
||||
# password: StringProperty(subtype='PASSWORD')
|
||||
# project_name: StringProperty()
|
||||
|
||||
def get_asset_path(self, name, catalog, directory=None):
|
||||
# chemin: Source, Asset_type, asset_name / asset_name.blend -> PolyHaven/HDRIs/test/test.blend
|
||||
directory = directory or self.source_directory
|
||||
catalog = self.norm_file_name(catalog)
|
||||
name = self.norm_file_name(name)
|
||||
|
||||
return Path(directory, self.get_asset_relative_path(name, catalog))
|
||||
|
||||
# def bundle(self, cache_diff=None):
|
||||
# """Group all asset in one or multiple blends for the asset browser"""
|
||||
|
||||
# return super().bundle(cache_diff=cache_diff)
|
||||
|
||||
def format_asset_info(self, asset_info, asset_path):
|
||||
# prend un asset info et output un asset description
|
||||
|
||||
asset_path = self.prop_rel_path(asset_path, 'source_directory')
|
||||
modified = asset_info.get('modified', time.time_ns())
|
||||
|
||||
return dict(
|
||||
filepath=asset_path,
|
||||
modified=modified,
|
||||
library_id=self.library.id,
|
||||
assets=[dict(
|
||||
catalog=asset_data.get('catalog', asset_info['catalog']),
|
||||
author=asset_data.get('author'),
|
||||
metadata=asset_data.get('metadata', {}),
|
||||
description=asset_data.get('description', ''),
|
||||
tags=asset_data.get('tags', []),
|
||||
type=self.data_type,
|
||||
image=self.template_image,
|
||||
video=self.template_video,
|
||||
name=asset_data['name']) for asset_data in asset_info['assets']
|
||||
]
|
||||
)
|
||||
|
||||
def fetch(self):
|
||||
"""Gather in a list all assets found in the folder"""
|
||||
|
||||
print(f'Fetch Assets for {self.library.name}')
|
||||
|
||||
print('self.asset_type: ', self.asset_type)
|
||||
url = f"https://api.polyhaven.com/assets?t={self.asset_type.lower()}"
|
||||
# url2 = f"https://polyhaven.com/{self.asset_type.lower()}"
|
||||
# url += "&future=true" if early_access else ""
|
||||
# verify_ssl = not bpy.context.preferences.addons["polyhavenassets"].preferences.disable_ssl_verify
|
||||
|
||||
verify_ssl = False
|
||||
try:
|
||||
res = requests.get(url, headers=REQ_HEADERS, verify=verify_ssl)
|
||||
res2 = requests.get(url2, headers=REQ_HEADERS, verify=verify_ssl)
|
||||
except Exception as e:
|
||||
msg = f"[{type(e).__name__}] Error retrieving {url}"
|
||||
print(msg)
|
||||
# return (msg, None)
|
||||
|
||||
if res.status_code != 200:
|
||||
error = f"Error retrieving asset list, status code: {res.status_code}"
|
||||
print(error)
|
||||
# return (error, None)
|
||||
|
||||
catalog = None
|
||||
|
||||
# return (None, res.json())
|
||||
for asset_info in res.json().values():
|
||||
main_category = None
|
||||
secondary_category = None
|
||||
for category in asset_info['categories']:
|
||||
if category in self.main_category and not main_category:
|
||||
main_category = category
|
||||
if category in self.secondary_category and not secondary_category:
|
||||
secondary_category = category
|
||||
|
||||
if main_category and secondary_category:
|
||||
catalog = f'{main_category}_{secondary_category}'
|
||||
|
||||
if not catalog:
|
||||
return
|
||||
|
||||
asset_path = self.get_asset_path(asset_info['name'], catalog)
|
||||
print('asset_path: ', asset_path)
|
||||
asset_info = self.format_asset_info(asset_info, asset_path)
|
||||
print('asset_info: ', asset_info)
|
||||
|
||||
# return self.format_asset_info([asset['name'], self.get_asset_path(asset['name'], catalog) for asset, asset_infos in res.json().items()])
|
||||
# pp(res.json())
|
||||
# pp(res2.json())
|
||||
# print(res2)
|
||||
|
||||
|
||||
# return asset_infos
|
||||
@@ -0,0 +1,302 @@
|
||||
|
||||
"""
|
||||
Plugin for making an asset library of all blender file found in a folder
|
||||
"""
|
||||
|
||||
import re
|
||||
import uuid
|
||||
import os
|
||||
import shutil
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from itertools import groupby
|
||||
|
||||
import bpy
|
||||
from bpy.props import (StringProperty, IntProperty, BoolProperty)
|
||||
|
||||
from .library_plugin import LibraryPlugin
|
||||
from ..core.bl_utils import load_datablocks
|
||||
from ..core.template import Template
|
||||
|
||||
|
||||
|
||||
class ScanFolder(LibraryPlugin):
|
||||
|
||||
name = "Scan Folder"
|
||||
source_directory : StringProperty(subtype='DIR_PATH')
|
||||
|
||||
source_template_file : StringProperty()
|
||||
source_template_image : StringProperty()
|
||||
source_template_video : StringProperty()
|
||||
source_template_info : StringProperty()
|
||||
|
||||
def draw_prefs(self, layout):
|
||||
layout.prop(self, "source_directory", text="Source: Directory")
|
||||
|
||||
col = layout.column(align=True)
|
||||
col.prop(self, "source_template_file", icon='COPY_ID', text='Template file')
|
||||
col.prop(self, "source_template_image", icon='COPY_ID', text='Template image')
|
||||
col.prop(self, "source_template_video", icon='COPY_ID', text='Template video')
|
||||
col.prop(self, "source_template_info", icon='COPY_ID', text='Template info')
|
||||
|
||||
def get_asset_path(self, name, catalog, directory=None):
|
||||
directory = directory or self.source_directory
|
||||
catalog = self.norm_file_name(catalog)
|
||||
name = self.norm_file_name(name)
|
||||
|
||||
return Path(directory, self.get_asset_relative_path(name, catalog))
|
||||
|
||||
def get_image_path(self, name, catalog, filepath):
|
||||
catalog = self.norm_file_name(catalog)
|
||||
name = self.norm_file_name(name)
|
||||
return self.format_path(self.source_template_image, dict(name=name, catalog=catalog, filepath=filepath))
|
||||
|
||||
def get_video_path(self, name, catalog, filepath):
|
||||
catalog = self.norm_file_name(catalog)
|
||||
name = self.norm_file_name(name)
|
||||
return self.format_path(self.source_template_video, dict(name=name, catalog=catalog, filepath=filepath))
|
||||
|
||||
def new_asset(self, asset, asset_data):
|
||||
raise Exception('Need to be defined in the plugin')
|
||||
|
||||
def remove_asset(self, asset, asset_data):
|
||||
raise Exception('Need to be defined in the plugin')
|
||||
|
||||
'''
|
||||
def format_asset_info(self, asset_datas, asset_path, modified=None):
|
||||
|
||||
asset_path = self.prop_rel_path(asset_path, 'source_directory')
|
||||
modified = modified or time.time_ns()
|
||||
library_id = self.library.id
|
||||
|
||||
# if self.data_type == 'FILE':
|
||||
# return dict(
|
||||
# filepath=asset_path,
|
||||
# author=asset_info.get('author'),
|
||||
# modified=modified,
|
||||
# library_id=library_id,
|
||||
# catalog=asset_info['catalog'],
|
||||
# tags=[],
|
||||
# description=asset_info.get('description', ''),
|
||||
# type=self.data_type,
|
||||
# #image=self.source_template_image,
|
||||
# name=asset_info['name']
|
||||
# )
|
||||
|
||||
return dict(
|
||||
filepath=asset_path,
|
||||
modified=modified,
|
||||
library_id=library_id,
|
||||
assets=[dict(
|
||||
catalog=asset_data['catalog'],
|
||||
author=asset_data.get('author', ''),
|
||||
metadata=asset_data.get('metadata', {}),
|
||||
description=asset_data.get('description', ''),
|
||||
tags=asset_data.get('tags', []),
|
||||
type=self.data_type,
|
||||
name=asset_data['name']) for asset_data in asset_datas
|
||||
]
|
||||
)
|
||||
'''
|
||||
|
||||
def set_asset_preview(self, asset, asset_cache):
|
||||
'''Load an externalize image as preview for an asset using the source template'''
|
||||
|
||||
asset_path = self.format_path(asset_cache.filepath)
|
||||
|
||||
image_template = self.source_template_image
|
||||
if not image_template:
|
||||
return
|
||||
|
||||
image_path = self.find_path(image_template, asset_cache.to_dict(), filepath=asset_path)
|
||||
|
||||
if image_path:
|
||||
with bpy.context.temp_override(id=asset):
|
||||
bpy.ops.ed.lib_id_load_custom_preview(
|
||||
filepath=str(image_path)
|
||||
)
|
||||
else:
|
||||
print(f'No image found for {image_template} on {asset.name}')
|
||||
|
||||
if asset.preview:
|
||||
return asset.preview
|
||||
|
||||
def bundle(self, cache_diff=None):
|
||||
"""Group all new assets in one or multiple blends for the asset browser"""
|
||||
|
||||
if self.data_type not in ('FILE', 'ACTION', 'COLLECTION'):
|
||||
print(f'{self.data_type} is not supported yet')
|
||||
return
|
||||
|
||||
#catalog_data = self.read_catalog()
|
||||
|
||||
catalog = self.read_catalog()
|
||||
cache = None
|
||||
|
||||
if not cache_diff:
|
||||
# Get list of all modifications
|
||||
cache = self.fetch()
|
||||
cache_diff = cache.diff()
|
||||
|
||||
# Write the cache in a temporary file for the generate preview script
|
||||
tmp_cache_file = cache.write(tmp=True)
|
||||
bpy.ops.assetlibrary.generate_previews(name=self.library.name, cache=str(tmp_cache_file))
|
||||
|
||||
elif isinstance(cache_diff, (Path, str)):
|
||||
cache_diff = json.loads(Path(cache_diff).read_text(encoding='utf-8'))
|
||||
|
||||
if self.library.blend_depth == 0:
|
||||
raise Exception('Blender depth must be 1 at min')
|
||||
|
||||
total_assets = len(cache_diff)
|
||||
print(f'total_assets={total_assets}')
|
||||
|
||||
if total_assets == 0:
|
||||
print('No assets found')
|
||||
return
|
||||
|
||||
i = 0
|
||||
for blend_path, asset_cache_diffs in cache_diff.group_by(key=self.get_asset_bundle_path):
|
||||
if blend_path.exists():
|
||||
print(f'Opening existing bundle blend: {blend_path}')
|
||||
bpy.ops.wm.open_mainfile(filepath=str(blend_path))
|
||||
else:
|
||||
print(f'Create new bundle blend to: {blend_path}')
|
||||
bpy.ops.wm.read_homefile(use_empty=True)
|
||||
|
||||
for asset_cache_diff in asset_cache_diffs:
|
||||
if total_assets <= 100 or i % int(total_assets / 10) == 0:
|
||||
print(f'Progress: {int(i / total_assets * 100)+1}')
|
||||
|
||||
operation = asset_cache_diff.operation
|
||||
asset_cache = asset_cache_diff.asset_cache
|
||||
asset_name = asset_cache.name
|
||||
asset = getattr(bpy.data, self.data_types).get(asset_name)
|
||||
|
||||
if operation == 'REMOVE':
|
||||
if asset:
|
||||
getattr(bpy.data, self.data_types).remove(asset)
|
||||
else:
|
||||
print(f'ERROR : Remove Asset: {asset_name} not found in {blend_path}')
|
||||
continue
|
||||
|
||||
if asset_cache_diff.operation == 'MODIFY' and not asset:
|
||||
print(f'WARNING: Modifiy Asset: {asset_name} not found in {blend_path} it will be created')
|
||||
|
||||
if operation == 'ADD' or not asset:
|
||||
if asset:
|
||||
#raise Exception(f"Asset {asset_name} Already in Blend")
|
||||
print(f"Asset {asset_name} Already in Blend")
|
||||
getattr(bpy.data, self.data_types).remove(asset)
|
||||
|
||||
#print(f"INFO: Add new asset: {asset_name}")
|
||||
asset = getattr(bpy.data, self.data_types).new(name=asset_name)
|
||||
else:
|
||||
print(f'operation {operation} not supported should be in (ADD, REMOVE, MODIFY)')
|
||||
continue
|
||||
|
||||
asset.asset_mark()
|
||||
asset.asset_data.catalog_id = catalog.add(asset_cache_diff.catalog).id
|
||||
|
||||
self.set_asset_preview(asset, asset_cache)
|
||||
self.set_asset_metadata(asset, asset_cache)
|
||||
self.set_asset_tags(asset, asset_cache)
|
||||
self.set_asset_info(asset, asset_cache)
|
||||
|
||||
i += 1
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# If the variable cache_diff was given we need to update the cache with the diff
|
||||
if cache is None:
|
||||
cache = self.read_cache()
|
||||
cache.update(cache_diff)
|
||||
|
||||
cache.write()
|
||||
|
||||
catalog.update(cache.catalogs)
|
||||
catalog.write()
|
||||
|
||||
bpy.ops.wm.quit_blender()
|
||||
|
||||
def fetch(self):
|
||||
"""Gather in a list all assets found in the folder"""
|
||||
|
||||
print(f'Fetch Assets for {self.library.name}')
|
||||
|
||||
source_directory = Path(self.source_directory)
|
||||
template_file = Template(self.source_template_file)
|
||||
#catalog_data = self.read_catalog(directory=source_directory)
|
||||
#catalog_ids = {v['id']: k for k, v in catalog_data.items()}
|
||||
|
||||
#self.catalog.read()
|
||||
|
||||
cache = self.read_cache()
|
||||
|
||||
print(f'Search for blend using glob template: {template_file.glob_pattern}')
|
||||
print(f'Scanning Folder {source_directory}...')
|
||||
|
||||
#new_cache = LibraryCache()
|
||||
|
||||
for asset_path in template_file.glob(source_directory):
|
||||
|
||||
source_rel_path = self.prop_rel_path(asset_path, 'source_directory')
|
||||
modified = asset_path.stat().st_mtime_ns
|
||||
|
||||
# Check if the asset description as already been cached
|
||||
file_cache = next((a for a in cache if a.filepath == source_rel_path), None)
|
||||
|
||||
if file_cache:
|
||||
if file_cache.modified >= modified: #print(asset_path, 'is skipped because not modified')
|
||||
continue
|
||||
else:
|
||||
file_cache = cache.add(filepath=source_rel_path)
|
||||
|
||||
rel_path = asset_path.relative_to(source_directory).as_posix()
|
||||
field_data = template_file.parse(rel_path)
|
||||
|
||||
# Create the catalog path from the actual path of the asset
|
||||
catalog = [v for k,v in sorted(field_data.items()) if re.findall('cat[0-9]+', k)]
|
||||
#catalogs = [c.replace('_', ' ').title() for c in catalogs]
|
||||
|
||||
asset_name = field_data.get('asset_name', asset_path.stem)
|
||||
|
||||
if self.data_type == 'FILE':
|
||||
file_cache.set_data(
|
||||
name=asset_name,
|
||||
type='FILE',
|
||||
catalog=catalog,
|
||||
modified=modified
|
||||
)
|
||||
|
||||
continue
|
||||
|
||||
# Now check if there is a asset description file (Commented for now propably not usefull)
|
||||
#asset_info_path = self.find_path(self.source_template_info, asset_info, filepath=asset_path)
|
||||
#if asset_info_path:
|
||||
# new_cache.append(self.read_file(asset_info_path))
|
||||
# continue
|
||||
|
||||
# Scan the blend file for assets inside
|
||||
print(f'Scanning blendfile {asset_path}...')
|
||||
assets = self.load_datablocks(asset_path, type=self.data_types, link=True, assets_only=True)
|
||||
print(f'Found {len(assets)} {self.data_types} inside')
|
||||
|
||||
for asset in assets:
|
||||
#catalog_path = catalog_ids.get(asset.asset_data.catalog_id)
|
||||
|
||||
#if not catalog_path:
|
||||
# print(f'No catalog found for asset {asset.name}')
|
||||
#catalog_path = asset_info['catalog']#asset_path.relative_to(self.source_directory).as_posix()
|
||||
|
||||
# For now the catalog used is the one extract from the template file
|
||||
file_cache.assets.add(self.get_asset_data(asset), catalog=catalog)
|
||||
getattr(bpy.data, self.data_types).remove(asset)
|
||||
|
||||
return cache
|
||||
|
||||
Reference in New Issue
Block a user