Browsing now uses node objects, instead of just node UUID

This made the browsing code slightly simpler, and reduces the number
of Pillar queries. The list of nodes visited to end up at the current node
is also stored, so that we can easily go back up.
This commit is contained in:
Sybren A. Stüvel 2016-03-21 11:46:47 +01:00
parent 5f43b355b0
commit 5c2beaf7b2
2 changed files with 94 additions and 49 deletions

View File

@ -21,7 +21,6 @@
import asyncio import asyncio
import logging import logging
import threading import threading
import traceback
import bpy import bpy
import bgl import bgl
@ -33,6 +32,7 @@ from bpy.props import (BoolProperty, EnumProperty,
FloatProperty, FloatVectorProperty, FloatProperty, FloatVectorProperty,
IntProperty, StringProperty) IntProperty, StringProperty)
import pillarsdk
from . import async_loop, pillar from . import async_loop, pillar
icon_width = 128 icon_width = 128
@ -44,6 +44,13 @@ library_path = '/tmp'
library_icons_path = os.path.join(os.path.dirname(__file__), "icons") library_icons_path = os.path.join(os.path.dirname(__file__), "icons")
class UpNode(pillarsdk.Node):
def __init__(self):
super().__init__()
self['_id'] = 'UP'
self['node_type'] = 'UP'
class MenuItem: class MenuItem:
"""GUI menu item for the 3D View GUI.""" """GUI menu item for the 3D View GUI."""
@ -59,17 +66,19 @@ class MenuItem:
'SPINNER': os.path.join(library_icons_path, 'spinner.png'), 'SPINNER': os.path.join(library_icons_path, 'spinner.png'),
} }
def __init__(self, node_uuid: str, file_desc, thumb_path: str, label_text): SUPPORTED_NODE_TYPES = {'UP', 'group_texture', 'texture'}
# TODO: change node_uuid to the actual pillarsdk.Node object.
# That way we can simply inspect node.node_type to distinguish between
# folders ('group_texture' type) and files ('texture' type).
self.node_uuid = node_uuid # pillarsdk.Node UUID of a texture node def __init__(self, node, file_desc, thumb_path: str, label_text):
if node['node_type'] not in self.SUPPORTED_NODE_TYPES:
raise TypeError('Node of type %r not supported; supported are %r.' % (
node.group_texture, self.SUPPORTED_NODE_TYPES))
self.node = node # pillarsdk.Node, contains 'node_type' key to indicate type
self.file_desc = file_desc # pillarsdk.File object, or None if a 'folder' node. self.file_desc = file_desc # pillarsdk.File object, or None if a 'folder' node.
self.label_text = label_text self.label_text = label_text
self._thumb_path = '' self._thumb_path = ''
self.icon = None self.icon = None
self._is_folder = file_desc is None and thumb_path == 'FOLDER' self._is_folder = node['node_type'] == 'group_texture' or isinstance(node, UpNode)
self.thumb_path = thumb_path self.thumb_path = thumb_path
@ -91,7 +100,15 @@ class MenuItem:
else: else:
self.icon = None self.icon = None
def update(self, file_desc, thumb_path: str, label_text): @property
def node_uuid(self) -> str:
return self.node['_id']
def update(self, node, file_desc, thumb_path: str, label_text):
# We can get updated information about our Node, but a MenuItem should
# always represent one node, and it shouldn't be shared between nodes.
assert self.node_uuid == node['_id'], "Don't change the node ID this MenuItem reflects, just create a new one."
self.node = node
self.file_desc = file_desc # pillarsdk.File object, or None if a 'folder' node. self.file_desc = file_desc # pillarsdk.File object, or None if a 'folder' node.
self.thumb_path = thumb_path self.thumb_path = thumb_path
self.label_text = label_text self.label_text = label_text
@ -166,7 +183,13 @@ class BlenderCloudBrowser(bpy.types.Operator):
_state = 'BROWSING' _state = 'BROWSING'
project_uuid = '5672beecc0261b2005ed1a33' # Blender Cloud project UUID project_uuid = '5672beecc0261b2005ed1a33' # Blender Cloud project UUID
node_uuid = '' # Blender Cloud node UUID node = None # The Node object we're currently showing, or None if we're at the project top.
node_uuid = '' # Blender Cloud node UUID we're currently showing, i.e. None-safe self.node['_id']
# This contains a stack of Node objects that lead up to the currently browsed node.
# This allows us to display the "up" item.
path_stack = []
async_task = None # asyncio task for fetching thumbnails async_task = None # asyncio task for fetching thumbnails
signalling_future = None # asyncio future for signalling that we want to cancel everything. signalling_future = None # asyncio future for signalling that we want to cancel everything.
timer = None timer = None
@ -187,6 +210,7 @@ class BlenderCloudBrowser(bpy.types.Operator):
self.thumbnails_cache = wm.thumbnails_cache self.thumbnails_cache = wm.thumbnails_cache
self.project_uuid = wm.blender_cloud_project self.project_uuid = wm.blender_cloud_project
self.node_uuid = wm.blender_cloud_node self.node_uuid = wm.blender_cloud_node
self.path_stack = []
self.mouse_x = event.mouse_x self.mouse_x = event.mouse_x
self.mouse_y = event.mouse_y self.mouse_y = event.mouse_y
@ -237,8 +261,7 @@ class BlenderCloudBrowser(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
if selected.is_folder: if selected.is_folder:
self.node_uuid = selected.node_uuid self.descend_node(selected.node)
self.browse_assets()
else: else:
if selected.file_desc is None: if selected.file_desc is None:
# This can happen when the thumbnail information isn't loaded yet. # This can happen when the thumbnail information isn't loaded yet.
@ -253,6 +276,27 @@ class BlenderCloudBrowser(bpy.types.Operator):
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
def descend_node(self, node):
"""Descends the node hierarchy by visiting this node.
Also keeps track of the current node, so that we know where the "up" button should go.
"""
# Going up or down?
if self.path_stack and isinstance(node, UpNode):
self.log.debug('Going up, pop the stack; pre-pop stack is %r', self.path_stack)
node = self.path_stack.pop()
else:
# Going down, keep track of where we were (project top-level is None)
self.path_stack.append(self.node)
self.log.debug('Going up, push the stack; post-push stack is %r', self.path_stack)
# Set 'current' to the given node
self.node_uuid = node['_id'] if node else None
self.node = node
self.browse_assets()
def _stop_async_task(self): def _stop_async_task(self):
self.log.debug('Stopping async task') self.log.debug('Stopping async task')
if self.async_task is None: if self.async_task is None:
@ -317,12 +361,14 @@ class BlenderCloudBrowser(bpy.types.Operator):
return menu_item return menu_item
def update_menu_item(self, node_uuid, *args) -> MenuItem: def update_menu_item(self, node, *args) -> MenuItem:
node_uuid = node['_id']
# Just make this thread-safe to be on the safe side. # Just make this thread-safe to be on the safe side.
with self._menu_item_lock: with self._menu_item_lock:
for menu_item in self.current_display_content: for menu_item in self.current_display_content:
if menu_item.node_uuid == node_uuid: if menu_item.node_uuid == node_uuid:
menu_item.update(*args) menu_item.update(node, *args)
self.loaded_images.add(menu_item.icon.filepath_raw) self.loaded_images.add(menu_item.icon.filepath_raw)
break break
else: else:
@ -332,46 +378,49 @@ class BlenderCloudBrowser(bpy.types.Operator):
self.log.info('Asynchronously downloading previews to %r', thumbnails_directory) self.log.info('Asynchronously downloading previews to %r', thumbnails_directory)
self.clear_images() self.clear_images()
def thumbnail_loading(node_uuid, texture_node): def thumbnail_loading(node, texture_node):
self.add_menu_item(node_uuid, None, 'SPINNER', texture_node['name']) self.add_menu_item(node, None, 'SPINNER', texture_node['name'])
def thumbnail_loaded(node_uuid, file_desc, thumb_path): def thumbnail_loaded(node, file_desc, thumb_path):
self.update_menu_item(node_uuid, file_desc, thumb_path, file_desc['filename']) self.update_menu_item(node, file_desc, thumb_path, file_desc['filename'])
# Download either by group_texture node UUID or by project UUID (which shows all top-level nodes)
if self.node_uuid: if self.node_uuid:
self.log.debug('Getting subnodes for parent node %r', self.node_uuid) self.log.debug('Getting subnodes for parent node %r', self.node_uuid)
children = await pillar.get_nodes(parent_node_uuid=self.node_uuid, children = await pillar.get_nodes(parent_node_uuid=self.node_uuid,
node_type='group_textures') node_type='group_textures')
self.log.debug('Finding parent of node %r', self.node_uuid)
# Make sure we can go up again. # Make sure we can go up again.
parent_uuid = await pillar.parent_node_uuid(self.node_uuid) if self.path_stack:
self.add_menu_item(parent_uuid, None, 'FOLDER', '.. up ..') self.add_menu_item(UpNode(), None, 'FOLDER', '.. up ..')
self.log.debug('Iterating over child nodes of %r', self.node_uuid)
for child in children:
# print(' - %(_id)s = %(name)s' % child)
self.add_menu_item(child['_id'], None, 'FOLDER', child['name'])
directory = os.path.join(thumbnails_directory, self.project_uuid, self.node_uuid)
os.makedirs(directory, exist_ok=True)
self.log.debug('Fetching texture thumbnails for node %r', self.node_uuid)
await pillar.fetch_texture_thumbs(self.node_uuid, 's', directory,
thumbnail_loading=thumbnail_loading,
thumbnail_loaded=thumbnail_loaded,
future=self.signalling_future)
elif self.project_uuid: elif self.project_uuid:
self.log.debug('Getting subnodes for project node %r', self.project_uuid) self.log.debug('Getting subnodes for project node %r', self.project_uuid)
children = await pillar.get_nodes(self.project_uuid, '') children = await pillar.get_nodes(self.project_uuid, '')
self.log.debug('Iterating over child nodes of project %r', self.project_uuid)
for child in children:
# print(' - %(_id)s = %(name)s' % child)
self.add_menu_item(child['_id'], None, 'FOLDER', child['name'])
else: else:
# TODO: add "nothing here" icon and trigger re-draw # TODO: add "nothing here" icon and trigger re-draw
self.log.warning("Not node UUID and no project UUID, I can't do anything!") self.log.warning("Not node UUID and no project UUID, I can't do anything!")
return
# Download all child nodes
self.log.debug('Iterating over child nodes of %r', self.node_uuid)
for child in children:
# print(' - %(_id)s = %(name)s' % child)
self.add_menu_item(child, None, 'FOLDER', child['name'])
# There are only sub-nodes at the project level, no texture nodes,
# so we won't have to bother looking for textures.
if not self.node_uuid:
return
directory = os.path.join(thumbnails_directory, self.project_uuid, self.node_uuid)
os.makedirs(directory, exist_ok=True)
self.log.debug('Fetching texture thumbnails for node %r', self.node_uuid)
await pillar.fetch_texture_thumbs(self.node_uuid, 's', directory,
thumbnail_loading=thumbnail_loading,
thumbnail_loaded=thumbnail_loaded,
future=self.signalling_future)
def browse_assets(self): def browse_assets(self):
self._state = 'BROWSING' self._state = 'BROWSING'

View File

@ -12,7 +12,6 @@ import pillarsdk.utils
from . import http_cache from . import http_cache
_pillar_api = None # will become a pillarsdk.Api object. _pillar_api = None # will become a pillarsdk.Api object.
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
uncached_session = requests.session() uncached_session = requests.session()
@ -43,7 +42,7 @@ def blender_id_profile() -> dict:
return blender_id.profiles.get_active_profile() return blender_id.profiles.get_active_profile()
def pillar_api(pillar_endpoint: str=None) -> pillarsdk.Api: def pillar_api(pillar_endpoint: str = None) -> pillarsdk.Api:
"""Returns the Pillar SDK API object for the current user. """Returns the Pillar SDK API object for the current user.
The user must be logged in. The user must be logged in.
@ -127,7 +126,8 @@ async def get_nodes(project_uuid: str = None, parent_node_uuid: str = None,
'properties.order': 1, 'properties.status': 1, 'properties.order': 1, 'properties.status': 1,
'properties.content_type': 1, 'picture': 1}, 'properties.content_type': 1, 'picture': 1},
'where': where, 'where': where,
'sort': 'properties.order'}, api=pillar_api()) 'sort': 'properties.order',
'embed': ['parent']}, api=pillar_api())
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
children = await loop.run_in_executor(None, node_all) children = await loop.run_in_executor(None, node_all)
@ -226,10 +226,10 @@ async def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str,
@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. @param thumbnail_directory: directory in which to store the downloaded thumbnails.
@param thumbnail_loading: callback function that takes (node_id, pillarsdk.File object) @param thumbnail_loading: callback function that takes (pillarsdk.Node, pillarsdk.File)
parameters, which is called before a thumbnail will be downloaded. This allows you to parameters, which is called before a thumbnail will be downloaded. This allows you to
show a "downloading" indicator. show a "downloading" indicator.
@param thumbnail_loaded: callback function that takes (node_id, pillarsdk.File object, @param thumbnail_loaded: callback function that takes (pillarsdk.Node, pillarsdk.File object,
thumbnail path) parameters, which is called for every thumbnail after it's been downloaded. thumbnail path) parameters, which is called for every thumbnail after it's been downloaded.
@param future: Future that's inspected; if it is not None and cancelled, texture downloading @param future: Future that's inspected; if it is not None and cancelled, texture downloading
is aborted. is aborted.
@ -254,9 +254,7 @@ async def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str,
# Find the File that belongs to this texture node # Find the File that belongs to this texture node
pic_uuid = texture_node['picture'] pic_uuid = texture_node['picture']
loop.call_soon_threadsafe(functools.partial(thumbnail_loading, loop.call_soon_threadsafe(thumbnail_loading, texture_node, texture_node)
texture_node['_id'],
texture_node))
file_desc = await loop.run_in_executor(None, file_find, pic_uuid) file_desc = await loop.run_in_executor(None, file_find, pic_uuid)
if file_desc is None: if file_desc is None:
@ -279,9 +277,7 @@ async def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str,
await download_to_file(thumb_url, thumb_path, future=future) await download_to_file(thumb_url, thumb_path, future=future)
loop.call_soon_threadsafe(functools.partial(thumbnail_loaded, loop.call_soon_threadsafe(thumbnail_loaded, texture_node, file_desc, thumb_path)
texture_node['_id'],
file_desc, thumb_path))
# Download all texture nodes in parallel. # Download all texture nodes in parallel.
log.debug('Getting child nodes of node %r', parent_node_uuid) log.debug('Getting child nodes of node %r', parent_node_uuid)