From 01b73a0439643ab436a72c43911b517495493115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Wed, 9 Mar 2016 17:34:37 +0100 Subject: [PATCH] Added browsing group_texture nodes Added downloading of thumbnails too. Still primitive, thumbs are re-downloaded every time and all I/O blocks the Blender UI. --- blender_cloud/__init__.py | 149 +++++++++++++++++++++++++++++++++++++- blender_cloud/pillar.py | 53 +++++++++++--- 2 files changed, 188 insertions(+), 14 deletions(-) diff --git a/blender_cloud/__init__.py b/blender_cloud/__init__.py index 2e97c08..5c6ff73 100644 --- a/blender_cloud/__init__.py +++ b/blender_cloud/__init__.py @@ -32,17 +32,21 @@ bl_info = { "support": "TESTING" } +import os.path +import typing + # Support reloading if 'pillar' in locals(): import importlib + pillar = importlib.reload(pillar) else: from . import pillar - import bpy -from bpy.types import AddonPreferences, Operator, PropertyGroup -from bpy.props import PointerProperty, StringProperty +import bpy.utils.previews +from bpy.types import AddonPreferences, Operator, PropertyGroup, WindowManager +from bpy.props import PointerProperty, StringProperty, EnumProperty class BlenderCloudPreferences(AddonPreferences): @@ -126,13 +130,152 @@ class PillarCredentialsUpdate(Operator): return {'FINISHED'} +# We can store multiple preview collections here, +# however in this example we only store "main" +preview_collections = {} + + +def enum_previews_from_directory_items(self, context) -> typing.List[typing.AnyStr]: + """EnumProperty callback""" + + if context is None: + return [] + + wm = context.window_manager + project_uuid = wm.blender_cloud_project + node_uuid = wm.blender_cloud_node + + # Get the preview collection (defined in register func). + pcoll = preview_collections["blender_cloud"] + if pcoll.project_uuid == project_uuid and pcoll.node_uuid == node_uuid: + return pcoll.previews + + print('Loading previews for project {!r} node {!r}'.format(project_uuid, node_uuid)) + enum_items = [] + + # If we have a node UUID, we fetch the textures + # FIXME: support mixture of sub-nodes and textures under one node. + if node_uuid: + # Make sure we can go up again. + parent = pillar.parent_node_uuid(node_uuid) + enum_items.append(('node-{}'.format(parent), 'up', 'up', + 'FILE_FOLDER', + len(enum_items))) + + directory = os.path.join(wm.thumbnails_cache, project_uuid, node_uuid) + os.makedirs(directory, exist_ok=True) + + for file_desc, thumb_path in pillar.fetch_texture_thumbs(node_uuid, 's', directory): + thumb = pcoll.get(thumb_path) + if thumb is None: + thumb = pcoll.load(thumb_path, thumb_path, 'IMAGE') + enum_items.append(('thumb-{}'.format(thumb_path), file_desc['filename'], + thumb_path, + # TODO: get something here that allows downloading the texture + thumb.icon_id, + len(enum_items))) + elif project_uuid: + children = pillar.get_nodes(project_uuid, '') + + for child in children: + print(' - %(_id)s = %(name)s' % child) + enum_items.append(('node-{}'.format(child['_id']), child['name'], + 'description', + 'FILE_FOLDER', + len(enum_items))) + + pcoll.previews = enum_items + pcoll.project_uuid = project_uuid + pcoll.node_uuid = node_uuid + return pcoll.previews + + +def enum_previews_from_directory_update(self, context): + print('Updating from {!r}'.format(self.blender_cloud_thumbnails)) + + sel_type, sel_id = self.blender_cloud_thumbnails.split('-', 1) + + if sel_type == 'node': + # Go into this node + self.blender_cloud_node = sel_id + elif sel_type == 'thumb': + # Select this image + pass + else: + print("enum_previews_from_directory_update: Don't know what to do with {!r}" + .format(self.blender_cloud_thumbnails)) + + +class PreviewsExamplePanel(bpy.types.Panel): + """Creates a Panel in the Object properties window""" + + bl_label = "Previews Example Panel" + bl_idname = "OBJECT_PT_previews" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "object" + + def draw(self, context): + layout = self.layout + wm = context.window_manager + + row = layout.column() + row.prop(wm, "thumbnails_cache") + row.prop(wm, "blender_cloud_project") + row.prop(wm, "blender_cloud_node") + row.template_icon_view(wm, "blender_cloud_thumbnails", show_labels=True) + row.prop(wm, "blender_cloud_thumbnails") + + def register(): bpy.utils.register_module(__name__) + WindowManager.thumbnails_cache = StringProperty( + name="Thumbnails cache", + subtype='DIR_PATH', + default='/home/sybren/.cache/blender_cloud/thumbnails') + + WindowManager.blender_cloud_project = StringProperty( + name="Blender Cloud project UUID", + default='5672beecc0261b2005ed1a33') # TODO: don't hard-code this + + WindowManager.blender_cloud_node = StringProperty( + name="Blender Cloud node UUID", + default='') # empty == top-level of project + + WindowManager.blender_cloud_thumbnails = EnumProperty( + items=enum_previews_from_directory_items, + update=enum_previews_from_directory_update, + ) + + # Note that preview collections returned by bpy.utils.previews + # are regular Python objects - you can use them to store custom data. + # + # This is especially useful here, since: + # - It avoids us regenerating the whole enum over and over. + # - It can store enum_items' strings + # (remember you have to keep those strings somewhere in py, + # else they get freed and Blender references invalid memory!). + pcoll = bpy.utils.previews.new() + pcoll.previews = () + pcoll.project_uuid = '' + pcoll.node_uuid = '' + + preview_collections["blender_cloud"] = pcoll + def unregister(): bpy.utils.unregister_module(__name__) + del WindowManager.thumbnails_cache + del WindowManager.blender_cloud_project + del WindowManager.blender_cloud_node + del WindowManager.blender_cloud_thumbnails + + for pcoll in preview_collections.values(): + bpy.utils.previews.remove(pcoll) + preview_collections.clear() + if __name__ == "__main__": register() diff --git a/blender_cloud/pillar.py b/blender_cloud/pillar.py index 2926d84..c239ad7 100644 --- a/blender_cloud/pillar.py +++ b/blender_cloud/pillar.py @@ -1,6 +1,7 @@ import sys import os import concurrent.futures +import functools # Add our shipped Pillar SDK wheel to the Python path if not any('pillar_sdk' in path for path in sys.path): @@ -114,35 +115,65 @@ def get_nodes(project_uuid: str = None, parent_node_uuid: str = None) -> list: return children['_items'] -def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str): - """Fetches all texture thumbnails in a certain parent node. +def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str, thumbnail_directory: str): + """Generator, fetches all texture thumbnails in a certain parent node. @param parent_node_uuid: the UUID of the parent node. All sub-nodes will be downloaded. @param desired_size: size indicator, from 'sbtmlh'. + @param thumbnail_directory: directory in which to store the downloaded thumbnails. + @returns: generator that yields (pillarsdk.File object, thumbnail path) tuples """ - def fetch_thumbnail_from_node(texture_node: pillarsdk.Node, api: pillarsdk.Api): + api = pillar_api() + + def fetch_thumbnail_from_node(texture_node: pillarsdk.Node): # Fetch the File description JSON pic_uuid = texture_node['picture'] file_desc = pillarsdk.File.find(pic_uuid, { 'projection': {'filename': 1, 'variations': 1, 'width': 1, 'height': 1}, }, api=api) + if file_desc is None: + print('Unable to find picture {}'.format(pic_uuid)) + return None, None + # Save the thumbnail - thumb_path = file_desc.stream_thumb_to_file('/tmp', desired_size, api=api) + thumb_path = file_desc.stream_thumb_to_file(thumbnail_directory, desired_size, api=api) - return texture_node['name'], thumb_path + return file_desc, thumb_path - api = pillar_api() + texture_nodes = (node for node in get_nodes(parent_node_uuid=parent_node_uuid) + if node['node_type'] == 'texture') + # # Single-threaded, not maintained: + # for node in texture_nodes: + # node, file = fetch_thumbnail_from_node(node) + # print('Node {} has picture {}'.format(node, file)) + + # Multi-threaded: with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: # Queue up fetching of thumbnails - futures = [executor.submit(fetch_thumbnail_from_node, node, api) - for node in get_nodes(parent_node_uuid=parent_node_uuid) - if node['node_type'] == 'texture'] + futures = [executor.submit(fetch_thumbnail_from_node, node) + for node in texture_nodes] for future in futures: - node, file = future.result() - print('Node {} has picture {}'.format(node, file)) + file_desc, thumb_path = future.result() + yield file_desc, thumb_path print('Done downloading texture thumbnails') + + +@functools.lru_cache(128) +def parent_node_uuid(node_uuid: str) -> str: + """Returns the UUID of the node's parent node, or an empty string if this is the top level.""" + + api = pillar_api() + node = pillarsdk.Node.find(node_uuid, {'projection': {'parent': 1}}, api=api) + if node is None: + return '' + + print('Found node {}'.format(node)) + try: + return node['parent'] + except KeyError: + return ''