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:
parent
5f43b355b0
commit
5c2beaf7b2
@ -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'
|
||||||
|
@ -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)
|
||||||
|
Reference in New Issue
Block a user