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.
This commit is contained in:
parent
0f8cededb2
commit
01b73a0439
@ -32,17 +32,21 @@ bl_info = {
|
|||||||
"support": "TESTING"
|
"support": "TESTING"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
import typing
|
||||||
|
|
||||||
# Support reloading
|
# Support reloading
|
||||||
if 'pillar' in locals():
|
if 'pillar' in locals():
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
pillar = importlib.reload(pillar)
|
pillar = importlib.reload(pillar)
|
||||||
else:
|
else:
|
||||||
from . import pillar
|
from . import pillar
|
||||||
|
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import AddonPreferences, Operator, PropertyGroup
|
import bpy.utils.previews
|
||||||
from bpy.props import PointerProperty, StringProperty
|
from bpy.types import AddonPreferences, Operator, PropertyGroup, WindowManager
|
||||||
|
from bpy.props import PointerProperty, StringProperty, EnumProperty
|
||||||
|
|
||||||
|
|
||||||
class BlenderCloudPreferences(AddonPreferences):
|
class BlenderCloudPreferences(AddonPreferences):
|
||||||
@ -126,13 +130,152 @@ class PillarCredentialsUpdate(Operator):
|
|||||||
return {'FINISHED'}
|
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():
|
def register():
|
||||||
bpy.utils.register_module(__name__)
|
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():
|
def unregister():
|
||||||
bpy.utils.unregister_module(__name__)
|
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__":
|
if __name__ == "__main__":
|
||||||
register()
|
register()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
|
import functools
|
||||||
|
|
||||||
# Add our shipped Pillar SDK wheel to the Python path
|
# Add our shipped Pillar SDK wheel to the Python path
|
||||||
if not any('pillar_sdk' in path for path in sys.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']
|
return children['_items']
|
||||||
|
|
||||||
|
|
||||||
def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str):
|
def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str, thumbnail_directory: str):
|
||||||
"""Fetches all texture thumbnails in a certain parent node.
|
"""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 parent_node_uuid: the UUID of the parent node. All sub-nodes will be downloaded.
|
||||||
@param desired_size: size indicator, from 'sbtmlh'.
|
@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
|
# Fetch the File description JSON
|
||||||
pic_uuid = texture_node['picture']
|
pic_uuid = texture_node['picture']
|
||||||
file_desc = pillarsdk.File.find(pic_uuid, {
|
file_desc = pillarsdk.File.find(pic_uuid, {
|
||||||
'projection': {'filename': 1, 'variations': 1, 'width': 1, 'height': 1},
|
'projection': {'filename': 1, 'variations': 1, 'width': 1, 'height': 1},
|
||||||
}, api=api)
|
}, api=api)
|
||||||
|
|
||||||
|
if file_desc is None:
|
||||||
|
print('Unable to find picture {}'.format(pic_uuid))
|
||||||
|
return None, None
|
||||||
|
|
||||||
# Save the thumbnail
|
# 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:
|
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
|
||||||
# Queue up fetching of thumbnails
|
# Queue up fetching of thumbnails
|
||||||
futures = [executor.submit(fetch_thumbnail_from_node, node, api)
|
futures = [executor.submit(fetch_thumbnail_from_node, node)
|
||||||
for node in get_nodes(parent_node_uuid=parent_node_uuid)
|
for node in texture_nodes]
|
||||||
if node['node_type'] == 'texture']
|
|
||||||
|
|
||||||
for future in futures:
|
for future in futures:
|
||||||
node, file = future.result()
|
file_desc, thumb_path = future.result()
|
||||||
print('Node {} has picture {}'.format(node, file))
|
yield file_desc, thumb_path
|
||||||
|
|
||||||
print('Done downloading texture thumbnails')
|
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 ''
|
||||||
|
Reference in New Issue
Block a user