Start browsing at project overview, instead of inside one project.
Also moved from using project_uuid and node_uuid to using CloudPath objects.
This commit is contained in:
parent
07f28d3072
commit
64d36818fe
@ -30,14 +30,6 @@ class BlenderCloudPreferences(AddonPreferences):
|
|||||||
get=lambda self: PILLAR_SERVER_URL
|
get=lambda self: PILLAR_SERVER_URL
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: Move to the Scene properties?
|
|
||||||
project_uuid = bpy.props.StringProperty(
|
|
||||||
name='Project UUID',
|
|
||||||
description='UUID of the current Blender Cloud project',
|
|
||||||
default='5672beecc0261b2005ed1a33',
|
|
||||||
get=lambda self: '5672beecc0261b2005ed1a33'
|
|
||||||
)
|
|
||||||
|
|
||||||
local_texture_dir = StringProperty(
|
local_texture_dir = StringProperty(
|
||||||
name='Default Blender Cloud texture storage directory',
|
name='Default Blender Cloud texture storage directory',
|
||||||
subtype='DIR_PATH',
|
subtype='DIR_PATH',
|
||||||
@ -154,14 +146,9 @@ def register():
|
|||||||
|
|
||||||
addon_prefs = preferences()
|
addon_prefs = preferences()
|
||||||
|
|
||||||
WindowManager.blender_cloud_project = StringProperty(
|
WindowManager.last_blender_cloud_location = StringProperty(
|
||||||
name="Blender Cloud project UUID",
|
name="Last Blender Cloud browser location",
|
||||||
default=addon_prefs.project_uuid) # TODO: don't hard-code this
|
default="/")
|
||||||
|
|
||||||
WindowManager.blender_cloud_node = StringProperty(
|
|
||||||
name="Blender Cloud node UUID",
|
|
||||||
default='') # empty == top-level of project
|
|
||||||
|
|
||||||
|
|
||||||
def default_if_empty(scene, context):
|
def default_if_empty(scene, context):
|
||||||
"""The scene's local_texture_dir, if empty, reverts to the addon prefs."""
|
"""The scene's local_texture_dir, if empty, reverts to the addon prefs."""
|
||||||
|
@ -44,13 +44,27 @@ 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):
|
class SpecialFolderNode(pillarsdk.Node):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UpNode(SpecialFolderNode):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self['_id'] = 'UP'
|
self['_id'] = 'UP'
|
||||||
self['node_type'] = 'UP'
|
self['node_type'] = 'UP'
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectNode(SpecialFolderNode):
|
||||||
|
def __init__(self, project):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
assert isinstance(project, pillarsdk.Project), 'wrong type for project: %r' % type(project)
|
||||||
|
|
||||||
|
self.merge(project.to_dict())
|
||||||
|
self['node_type'] = 'PROJECT'
|
||||||
|
|
||||||
|
|
||||||
class MenuItem:
|
class MenuItem:
|
||||||
"""GUI menu item for the 3D View GUI."""
|
"""GUI menu item for the 3D View GUI."""
|
||||||
|
|
||||||
@ -66,7 +80,7 @@ class MenuItem:
|
|||||||
'SPINNER': os.path.join(library_icons_path, 'spinner.png'),
|
'SPINNER': os.path.join(library_icons_path, 'spinner.png'),
|
||||||
}
|
}
|
||||||
|
|
||||||
SUPPORTED_NODE_TYPES = {'UP', 'group_texture', 'texture'}
|
SUPPORTED_NODE_TYPES = {'UP', 'PROJECT', 'group_texture', 'texture'}
|
||||||
|
|
||||||
def __init__(self, node, file_desc, thumb_path: str, label_text):
|
def __init__(self, node, file_desc, thumb_path: str, label_text):
|
||||||
self.log = logging.getLogger('%s.MenuItem' % __name__)
|
self.log = logging.getLogger('%s.MenuItem' % __name__)
|
||||||
@ -75,12 +89,15 @@ class MenuItem:
|
|||||||
raise TypeError('Node of type %r not supported; supported are %r.' % (
|
raise TypeError('Node of type %r not supported; supported are %r.' % (
|
||||||
node['node_type'], self.SUPPORTED_NODE_TYPES))
|
node['node_type'], self.SUPPORTED_NODE_TYPES))
|
||||||
|
|
||||||
|
assert isinstance(node, pillarsdk.Node), 'wrong type for node: %r' % type(node)
|
||||||
|
assert isinstance(node['_id'], str), 'wrong type for node["_id"]: %r' % type(node['_id'])
|
||||||
self.node = node # pillarsdk.Node, contains 'node_type' key to indicate type
|
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 = node['node_type'] == 'group_texture' or isinstance(node, UpNode)
|
self._is_folder = node['node_type'] == 'group_texture' or \
|
||||||
|
isinstance(node, SpecialFolderNode)
|
||||||
|
|
||||||
self.thumb_path = thumb_path
|
self.thumb_path = thumb_path
|
||||||
|
|
||||||
@ -186,12 +203,9 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
|||||||
|
|
||||||
_state = 'INITIALIZING'
|
_state = 'INITIALIZING'
|
||||||
|
|
||||||
project_uuid = '5672beecc0261b2005ed1a33' # Blender Cloud project UUID
|
current_path = pillar.CloudPath('/')
|
||||||
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 contains a stack of Node objects that lead up to the currently browsed node.
|
||||||
# This allows us to display the "up" item.
|
|
||||||
path_stack = []
|
path_stack = []
|
||||||
|
|
||||||
async_task = None # asyncio task for fetching thumbnails
|
async_task = None # asyncio task for fetching thumbnails
|
||||||
@ -200,7 +214,6 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
|||||||
log = logging.getLogger('%s.BlenderCloudBrowser' % __name__)
|
log = logging.getLogger('%s.BlenderCloudBrowser' % __name__)
|
||||||
|
|
||||||
_menu_item_lock = threading.Lock()
|
_menu_item_lock = threading.Lock()
|
||||||
current_path = ''
|
|
||||||
current_display_content = []
|
current_display_content = []
|
||||||
loaded_images = set()
|
loaded_images = set()
|
||||||
thumbnails_cache = ''
|
thumbnails_cache = ''
|
||||||
@ -217,9 +230,9 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
|||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
wm = context.window_manager
|
wm = context.window_manager
|
||||||
self.project_uuid = wm.blender_cloud_project
|
|
||||||
self.node_uuid = wm.blender_cloud_node
|
self.current_path = pillar.CloudPath(wm.last_blender_cloud_location)
|
||||||
self.path_stack = []
|
self.path_stack = [] # list of nodes that make up the current path.
|
||||||
|
|
||||||
self.thumbnails_cache = cache.cache_directory('thumbnails')
|
self.thumbnails_cache = cache.cache_directory('thumbnails')
|
||||||
self.mouse_x = event.mouse_x
|
self.mouse_x = event.mouse_x
|
||||||
@ -355,21 +368,28 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
|||||||
Also keeps track of the current node, so that we know where the "up" button should go.
|
Also keeps track of the current node, so that we know where the "up" button should go.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
assert isinstance(node, pillarsdk.Node), 'Wrong type %s' % node
|
||||||
|
|
||||||
# Going up or down?
|
# Going up or down?
|
||||||
if self.path_stack and isinstance(node, UpNode):
|
if isinstance(node, UpNode):
|
||||||
self.log.debug('Going up, pop the stack; pre-pop stack is %r', self.path_stack)
|
self.log.debug('Going up to %r', self.current_path)
|
||||||
node = self.path_stack.pop()
|
self.current_path = self.current_path.parent
|
||||||
|
if self.path_stack:
|
||||||
|
self.path_stack.pop()
|
||||||
else:
|
else:
|
||||||
# Going down, keep track of where we were (project top-level is None)
|
# Going down, keep track of where we were
|
||||||
self.path_stack.append(self.node)
|
self.current_path /= node['_id']
|
||||||
self.log.debug('Going up, push the stack; post-push stack is %r', self.path_stack)
|
self.log.debug('Going down to %r', self.current_path)
|
||||||
|
self.path_stack.append(node)
|
||||||
|
|
||||||
# Set 'current' to the given node
|
|
||||||
self.node_uuid = node['_id'] if node else None
|
|
||||||
self.node = node
|
|
||||||
self.browse_assets()
|
self.browse_assets()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def node(self):
|
||||||
|
if not self.path_stack:
|
||||||
|
return None
|
||||||
|
return self.path_stack[-1]
|
||||||
|
|
||||||
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:
|
||||||
@ -456,6 +476,7 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
|||||||
|
|
||||||
thumbnails_directory = self.thumbnails_cache
|
thumbnails_directory = self.thumbnails_cache
|
||||||
self.log.info('Asynchronously downloading previews to %r', thumbnails_directory)
|
self.log.info('Asynchronously downloading previews to %r', thumbnails_directory)
|
||||||
|
self.log.info('Current BCloud path is %r', self.current_path)
|
||||||
self.clear_images()
|
self.clear_images()
|
||||||
|
|
||||||
def thumbnail_loading(node, texture_node):
|
def thumbnail_loading(node, texture_node):
|
||||||
@ -464,27 +485,33 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
|||||||
def thumbnail_loaded(node, file_desc, thumb_path):
|
def thumbnail_loaded(node, file_desc, thumb_path):
|
||||||
self.update_menu_item(node, file_desc, thumb_path, file_desc['filename'])
|
self.update_menu_item(node, file_desc, thumb_path, file_desc['filename'])
|
||||||
|
|
||||||
|
project_uuid = self.current_path.project_uuid
|
||||||
|
node_uuid = self.current_path.node_uuid
|
||||||
|
|
||||||
# Download either by group_texture node UUID or by project UUID (which
|
# Download either by group_texture node UUID or by project UUID (which
|
||||||
# shows all top-level nodes)
|
# shows all top-level nodes)
|
||||||
if self.node_uuid:
|
if node_uuid:
|
||||||
self.log.debug('Getting subnodes for parent node %r', self.node_uuid)
|
self.log.debug('Getting subnodes for parent node %r', node_uuid)
|
||||||
children = await pillar.get_nodes(parent_node_uuid=self.node_uuid,
|
children = await pillar.get_nodes(parent_node_uuid=node_uuid,
|
||||||
node_type='group_textures')
|
node_type='group_textures')
|
||||||
|
|
||||||
# Make sure we can go up again.
|
# Make sure we can go up again.
|
||||||
if self.path_stack:
|
if self.path_stack:
|
||||||
self.add_menu_item(UpNode(), None, 'FOLDER', '.. up ..')
|
self.add_menu_item(UpNode(), None, 'FOLDER', '.. up ..')
|
||||||
elif self.project_uuid:
|
elif project_uuid:
|
||||||
self.log.debug('Getting subnodes for project node %r', self.project_uuid)
|
self.log.debug('Getting subnodes for project node %r', project_uuid)
|
||||||
children = await pillar.get_nodes(self.project_uuid, '')
|
children = await pillar.get_nodes(project_uuid, '')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# TODO: add "nothing here" icon and trigger re-draw
|
# Query for projects
|
||||||
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!")
|
||||||
|
children = await pillar.get_texture_projects()
|
||||||
|
for proj_dict in children:
|
||||||
|
self.add_menu_item(ProjectNode(proj_dict), None, 'FOLDER', proj_dict['name'])
|
||||||
return
|
return
|
||||||
|
|
||||||
# Download all child nodes
|
# Download all child nodes
|
||||||
self.log.debug('Iterating over child nodes of %r', self.node_uuid)
|
self.log.debug('Iterating over child nodes of %r', node_uuid)
|
||||||
for child in children:
|
for child in children:
|
||||||
# print(' - %(_id)s = %(name)s' % child)
|
# print(' - %(_id)s = %(name)s' % child)
|
||||||
if child['node_type'] not in MenuItem.SUPPORTED_NODE_TYPES:
|
if child['node_type'] not in MenuItem.SUPPORTED_NODE_TYPES:
|
||||||
@ -494,20 +521,20 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
|||||||
|
|
||||||
# There are only sub-nodes at the project level, no texture nodes,
|
# There are only sub-nodes at the project level, no texture nodes,
|
||||||
# so we won't have to bother looking for textures.
|
# so we won't have to bother looking for textures.
|
||||||
if not self.node_uuid:
|
if not node_uuid:
|
||||||
return
|
return
|
||||||
|
|
||||||
directory = os.path.join(thumbnails_directory, self.project_uuid, self.node_uuid)
|
directory = os.path.join(thumbnails_directory, project_uuid, node_uuid)
|
||||||
os.makedirs(directory, exist_ok=True)
|
os.makedirs(directory, exist_ok=True)
|
||||||
|
|
||||||
self.log.debug('Fetching texture thumbnails for node %r', self.node_uuid)
|
self.log.debug('Fetching texture thumbnails for node %r', node_uuid)
|
||||||
await pillar.fetch_texture_thumbs(self.node_uuid, 's', directory,
|
await pillar.fetch_texture_thumbs(node_uuid, 's', directory,
|
||||||
thumbnail_loading=thumbnail_loading,
|
thumbnail_loading=thumbnail_loading,
|
||||||
thumbnail_loaded=thumbnail_loaded,
|
thumbnail_loaded=thumbnail_loaded,
|
||||||
future=self.signalling_future)
|
future=self.signalling_future)
|
||||||
|
|
||||||
def browse_assets(self):
|
def browse_assets(self):
|
||||||
self.log.debug('Browsing assets at project %r node %r', self.project_uuid, self.node_uuid)
|
self.log.debug('Browsing assets at %r', self.current_path)
|
||||||
self._new_async_task(self.async_download_previews())
|
self._new_async_task(self.async_download_previews())
|
||||||
|
|
||||||
def _new_async_task(self, async_task: asyncio.coroutine, future: asyncio.Future = None):
|
def _new_async_task(self, async_task: asyncio.coroutine, future: asyncio.Future = None):
|
||||||
@ -694,8 +721,9 @@ class BlenderCloudBrowser(bpy.types.Operator):
|
|||||||
self.clear_images()
|
self.clear_images()
|
||||||
self._state = 'DOWNLOADING_TEXTURE'
|
self._state = 'DOWNLOADING_TEXTURE'
|
||||||
|
|
||||||
|
project_uuid = self.current_path.project_uuid
|
||||||
node_path_components = [node['name'] for node in self.path_stack if node is not None]
|
node_path_components = [node['name'] for node in self.path_stack if node is not None]
|
||||||
local_path_components = [self.project_uuid] + node_path_components + [self.node['name']]
|
local_path_components = [project_uuid] + node_path_components
|
||||||
|
|
||||||
top_texture_directory = bpy.path.abspath(context.scene.local_texture_dir)
|
top_texture_directory = bpy.path.abspath(context.scene.local_texture_dir)
|
||||||
local_path = os.path.join(top_texture_directory, *local_path_components)
|
local_path = os.path.join(top_texture_directory, *local_path_components)
|
||||||
|
@ -63,6 +63,8 @@ class CloudPath(pathlib.PurePosixPath):
|
|||||||
@property
|
@property
|
||||||
def project_uuid(self) -> str:
|
def project_uuid(self) -> str:
|
||||||
assert self.parts[0] == '/'
|
assert self.parts[0] == '/'
|
||||||
|
if len(self.parts) <= 1:
|
||||||
|
return None
|
||||||
return self.parts[1]
|
return self.parts[1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -72,11 +74,10 @@ class CloudPath(pathlib.PurePosixPath):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def node_uuid(self) -> str:
|
def node_uuid(self) -> str:
|
||||||
node_uuids = self.node_uuids
|
if len(self.parts) <= 2:
|
||||||
|
|
||||||
if not node_uuids:
|
|
||||||
return None
|
return None
|
||||||
return node_uuids[-1]
|
|
||||||
|
return self.parts[-1]
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
@ -270,6 +271,21 @@ async def get_nodes(project_uuid: str = None, parent_node_uuid: str = None,
|
|||||||
return children['_items']
|
return children['_items']
|
||||||
|
|
||||||
|
|
||||||
|
async def get_texture_projects() -> list:
|
||||||
|
"""Returns project dicts that contain textures."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
children = await pillar_call(pillarsdk.Project.all, {
|
||||||
|
'where': {'node_types.name': 'texture'},
|
||||||
|
'sort': 'name',
|
||||||
|
})
|
||||||
|
except pillarsdk.ResourceNotFound as ex:
|
||||||
|
log.warning('Unable to find texture projects: %s', ex)
|
||||||
|
raise PillarError('Unable to find texture projects: %s' % ex)
|
||||||
|
|
||||||
|
return children['_items']
|
||||||
|
|
||||||
|
|
||||||
async def download_to_file(url, filename, *,
|
async def download_to_file(url, filename, *,
|
||||||
header_store: str,
|
header_store: str,
|
||||||
chunk_size=100 * 1024,
|
chunk_size=100 * 1024,
|
||||||
@ -381,7 +397,7 @@ async def fetch_thumbnail_info(file: pillarsdk.File, directory: str, desired_siz
|
|||||||
finished.
|
finished.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
thumb_link = await pillar_call(file.thumbnail_file, desired_size)
|
thumb_link = await pillar_call(file.thumbnail, desired_size)
|
||||||
|
|
||||||
if thumb_link is None:
|
if thumb_link is None:
|
||||||
raise ValueError("File {} has no thumbnail of size {}"
|
raise ValueError("File {} has no thumbnail of size {}"
|
||||||
|
Reference in New Issue
Block a user