Compare commits

...

24 Commits

Author SHA1 Message Date
5f5f0d8db9 Prevent double map types in the filename. 2016-05-20 16:20:33 +02:00
30f71ac9fc Fixed typo. I'm a moron. 2016-05-20 14:34:35 +02:00
bdef942b0b Replaced log.warning with debug msg.
We can now list all available projects, so there is no need to warn.
2016-05-20 11:33:26 +02:00
2a0ef39b12 Bumped SDK requirement to 1.2.0 2016-05-20 11:31:45 +02:00
c57a3bc902 Bumped version to 1.1.0 2016-05-18 16:36:56 +02:00
b94998d12e Fall back on texture.properties.files[0].file if texture.picture doesn't exist. 2016-05-18 16:27:04 +02:00
1cd42e246e Use current_path in log 2016-05-18 15:55:08 +02:00
079689a532 Client-side sorting of nodes.
The sorting happens after obtaining the individual nodes, as this is done
in parallel in unpredictable order.
2016-05-18 15:13:44 +02:00
597ba6de1c Use project name in download path, rather than UUID.
Filenames are now also sanitized.
2016-05-18 15:13:29 +02:00
7b59391872 Place map type (col, spec, etc) at end of filename instead of start. 2016-05-18 14:14:38 +02:00
8201ba7691 Fix node type name 2016-05-18 14:12:21 +02:00
8f2b0f8faa Allow querying for multiple node types. 2016-05-18 14:11:49 +02:00
33b52cc8a9 CPU-friendlier by lowering fixed redraw rate.
The GUI is still redrawn on other events, such as mouse move, so it still
responds quickly to that. This is just regarding background updates of the
data model, such as when loading thumbnails.
2016-05-18 13:01:48 +02:00
be46b9cf81 Handling more cases of login/credentials issues 2016-05-18 13:01:04 +02:00
ba4c951d32 Use /bcloud/texture-library end point to fetch texture library projects. 2016-05-18 12:50:51 +02:00
5c7343f8c9 Make sure we can always go up again (except at top level) 2016-05-18 12:17:07 +02:00
64d36818fe Start browsing at project overview, instead of inside one project.
Also moved from using project_uuid and node_uuid to using CloudPath
objects.
2016-05-18 11:57:36 +02:00
07f28d3072 Debug log reason why module can't be imported.
Usually this will be because someone just wants to use the wheel, but
during development this can be caused by other issues, and shouldn't
be silenced.
2016-05-18 11:57:36 +02:00
48ca91a364 Skip nodes of unsupported node_type (instead of raising exception) 2016-05-17 17:30:57 +02:00
7ee052f71b Use project UUID from prefs 2016-05-17 17:30:38 +02:00
2bb859efd9 Increased pillarsdk required version 2016-05-10 15:04:49 +02:00
ac3943fe6c Bumped version to 1.0.1 2016-05-10 15:01:15 +02:00
5eaee872bf Added check for user's roles -- disallow usage by non-subscribers.
This makes it clear from the get-go that users need to subscribe. Otherwise
they'll get unexpected errors once they try to download something.
2016-05-10 14:52:51 +02:00
6ce4399407 Show default mouse cursor, instead of the one belonging to the editor. 2016-05-10 14:33:02 +02:00
7 changed files with 251 additions and 104 deletions

View File

@@ -21,7 +21,7 @@
bl_info = { bl_info = {
'name': 'Blender Cloud Texture Browser', 'name': 'Blender Cloud Texture Browser',
'author': 'Sybren A. Stüvel and Francesco Siddi', 'author': 'Sybren A. Stüvel and Francesco Siddi',
'version': (0, 2, 0), 'version': (1, 1, 0),
'blender': (2, 77, 0), 'blender': (2, 77, 0),
'location': 'Ctrl+Shift+Alt+A anywhere', 'location': 'Ctrl+Shift+Alt+A anywhere',
'description': 'Allows downloading of textures from the Blender Cloud. Requires ' 'description': 'Allows downloading of textures from the Blender Cloud. Requires '

View File

@@ -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',
@@ -152,16 +144,12 @@ def register():
bpy.utils.register_class(BlenderCloudPreferences) bpy.utils.register_class(BlenderCloudPreferences)
bpy.utils.register_class(PillarCredentialsUpdate) bpy.utils.register_class(PillarCredentialsUpdate)
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
addon_prefs = preferences() addon_prefs = preferences()
WindowManager.last_blender_cloud_location = StringProperty(
name="Last Blender Cloud browser location",
default="/")
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."""

View File

@@ -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,19 +80,30 @@ 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__)
if node['node_type'] not in self.SUPPORTED_NODE_TYPES: if node['node_type'] not in self.SUPPORTED_NODE_TYPES:
self.log.info('Invalid node type in node: %s', node)
raise TypeError('Node of type %r not supported; supported are %r.' % ( raise TypeError('Node of type %r not supported; supported are %r.' % (
node.group_texture, 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))
# Determine sorting order.
# by default, sort all the way at the end and folders first.
self._order = 0 if self._is_folder else 10000
if node and node.properties and node.properties.order is not None:
self._order = node.properties.order
self.thumb_path = thumb_path self.thumb_path = thumb_path
@@ -88,6 +113,10 @@ class MenuItem:
self.width = 0 self.width = 0
self.height = 0 self.height = 0
def sort_key(self):
"""Key for sorting lists of MenuItems."""
return self._order, self.label_text
@property @property
def thumb_path(self) -> str: def thumb_path(self) -> str:
return self._thumb_path return self._thumb_path
@@ -184,12 +213,10 @@ 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. project_name = ''
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
@@ -198,7 +225,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 = ''
@@ -215,9 +241,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
@@ -237,8 +263,9 @@ class BlenderCloudBrowser(bpy.types.Operator):
self.loaded_images = set() self.loaded_images = set()
self.check_credentials() self.check_credentials()
context.window.cursor_modal_set('DEFAULT')
context.window_manager.modal_handler_add(self) context.window_manager.modal_handler_add(self)
self.timer = context.window_manager.event_timer_add(1 / 30, context.window) self.timer = context.window_manager.event_timer_add(1 / 15, context.window)
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
@@ -268,9 +295,21 @@ class BlenderCloudBrowser(bpy.types.Operator):
self.mouse_x = event.mouse_x self.mouse_x = event.mouse_x
self.mouse_y = event.mouse_y self.mouse_y = event.mouse_y
if self._state == 'BROWSING' and event.type == 'LEFTMOUSE' and event.value == 'RELEASE': left_mouse_release = event.type == 'LEFTMOUSE' and event.value == 'RELEASE'
if self._state == 'PLEASE_SUBSCRIBE' and left_mouse_release:
self.open_browser_subscribe()
self._finish(context)
return {'FINISHED'}
if self._state == 'BROWSING':
selected = self.get_clicked() selected = self.get_clicked()
if selected:
context.window.cursor_set('HAND')
else:
context.window.cursor_set('DEFAULT')
if left_mouse_release:
if selected is None: if selected is None:
# No item clicked, ignore it. # No item clicked, ignore it.
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
@@ -282,10 +321,11 @@ class BlenderCloudBrowser(bpy.types.Operator):
# This can happen when the thumbnail information isn't loaded yet. # This can happen when the thumbnail information isn't loaded yet.
# Just ignore the click for now. # Just ignore the click for now.
# TODO: think of a way to handle this properly. # TODO: think of a way to handle this properly.
self.log.debug('Selected item %r has no file_desc', selected)
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
self.handle_item_selection(context, selected) self.handle_item_selection(context, selected)
elif event.type in {'RIGHTMOUSE', 'ESC'}: if event.type in {'RIGHTMOUSE', 'ESC'}:
self._finish(context) self._finish(context)
return {'CANCELLED'} return {'CANCELLED'}
@@ -301,6 +341,10 @@ class BlenderCloudBrowser(bpy.types.Operator):
try: try:
await pillar.check_pillar_credentials() await pillar.check_pillar_credentials()
except pillar.NotSubscribedToCloudError:
self.log.info('User not subscribed to Blender Cloud.')
self._show_subscribe_screen()
return
except pillar.CredentialsNotSyncedError: except pillar.CredentialsNotSyncedError:
self.log.info('Credentials not synced, re-syncing automatically.') self.log.info('Credentials not synced, re-syncing automatically.')
else: else:
@@ -310,8 +354,12 @@ class BlenderCloudBrowser(bpy.types.Operator):
try: try:
await pillar.refresh_pillar_credentials() await pillar.refresh_pillar_credentials()
except pillar.NotSubscribedToCloudError:
self.log.info('User is not a Blender Cloud subscriber.')
self._show_subscribe_screen()
return
except pillar.UserNotLoggedInError: except pillar.UserNotLoggedInError:
self.error('User not logged in on Blender ID.') self.log.error('User not logged in on Blender ID.')
else: else:
self.log.info('Credentials refreshed and ok, browsing assets.') self.log.info('Credentials refreshed and ok, browsing assets.')
await self.async_download_previews() await self.async_download_previews()
@@ -320,27 +368,45 @@ class BlenderCloudBrowser(bpy.types.Operator):
raise pillar.UserNotLoggedInError() raise pillar.UserNotLoggedInError()
# self._new_async_task(self._check_credentials()) # self._new_async_task(self._check_credentials())
def _show_subscribe_screen(self):
"""Shows the "You need to subscribe" screen."""
self._state = 'PLEASE_SUBSCRIBE'
bpy.context.window.cursor_set('HAND')
def descend_node(self, node): def descend_node(self, node):
"""Descends the node hierarchy by visiting this 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. Also keeps track of the current node, so that we know where the "up" button should go.
""" """
# Going up or down? assert isinstance(node, pillarsdk.Node), 'Wrong type %s' % node
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()
if isinstance(node, UpNode):
# Going up.
self.log.debug('Going up to %r', self.current_path)
self.current_path = self.current_path.parent
if self.path_stack:
self.path_stack.pop()
if not self.path_stack:
self.project_name = ''
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) if isinstance(node, ProjectNode):
self.log.debug('Going up, push the stack; post-push stack is %r', self.path_stack) self.project_name = node['name']
self.current_path /= node['_id']
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:
@@ -378,6 +444,7 @@ class BlenderCloudBrowser(bpy.types.Operator):
context.space_data.draw_handler_remove(self._draw_handle, 'WINDOW') context.space_data.draw_handler_remove(self._draw_handle, 'WINDOW')
context.window_manager.event_timer_remove(self.timer) context.window_manager.event_timer_remove(self.timer)
context.window.cursor_modal_restore()
if self.maximized_area: if self.maximized_area:
bpy.ops.screen.screen_full_area(use_hide_panels=True) bpy.ops.screen.screen_full_area(use_hide_panels=True)
@@ -406,6 +473,8 @@ class BlenderCloudBrowser(bpy.types.Operator):
self.current_display_content.append(menu_item) self.current_display_content.append(menu_item)
self.loaded_images.add(menu_item.icon.filepath_raw) self.loaded_images.add(menu_item.icon.filepath_raw)
self.sort_menu()
return menu_item return menu_item
def update_menu_item(self, node, *args) -> MenuItem: def update_menu_item(self, node, *args) -> MenuItem:
@@ -421,11 +490,23 @@ class BlenderCloudBrowser(bpy.types.Operator):
else: else:
raise ValueError('Unable to find MenuItem(node_uuid=%r)' % node_uuid) raise ValueError('Unable to find MenuItem(node_uuid=%r)' % node_uuid)
self.sort_menu()
def sort_menu(self):
"""Sorts the self.current_display_content list."""
if not self.current_display_content:
return
with self._menu_item_lock:
self.current_display_content.sort(key=MenuItem.sort_key)
async def async_download_previews(self): async def async_download_previews(self):
self._state = 'BROWSING' self._state = 'BROWSING'
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):
@@ -434,50 +515,59 @@ 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'])
# Download either by group_texture node UUID or by project UUID (which project_uuid = self.current_path.project_uuid
# shows all top-level nodes) node_uuid = self.current_path.node_uuid
if 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,
node_type='group_textures')
# Make sure we can go up again.
if self.path_stack:
self.add_menu_item(UpNode(), None, 'FOLDER', '.. up ..')
elif self.project_uuid:
self.log.debug('Getting subnodes for project node %r', self.project_uuid)
children = await pillar.get_nodes(self.project_uuid, '')
if node_uuid:
# Query for sub-nodes of this node.
self.log.debug('Getting subnodes for parent node %r', node_uuid)
children = await pillar.get_nodes(parent_node_uuid=node_uuid,
node_type='group_texture')
elif project_uuid:
# Query for top-level nodes.
self.log.debug('Getting subnodes for project node %r', project_uuid)
children = await pillar.get_nodes(project_uuid=project_uuid,
parent_node_uuid='',
node_type='group_texture')
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.debug('No node UUID and no project UUID, listing all projects')
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
# Make sure we can go up again.
self.add_menu_item(UpNode(), None, 'FOLDER', '.. up ..')
# 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', self.current_path)
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:
self.log.debug('Skipping node of type %r', child['node_type'])
continue
self.add_menu_item(child, None, 'FOLDER', child['name']) self.add_menu_item(child, None, 'FOLDER', child['name'])
# 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):
"""Stops the currently running async task, and starts another one.""" """Stops the currently running async task, and starts another one."""
self.log.debug('Setting up a new task %r, so any existing task must be stopped', async_task) self.log.debug('Setting up a new task %r, so any existing task must be stopped', async_task)
@@ -499,6 +589,7 @@ class BlenderCloudBrowser(bpy.types.Operator):
'BROWSING': self._draw_browser, 'BROWSING': self._draw_browser,
'DOWNLOADING_TEXTURE': self._draw_downloading, 'DOWNLOADING_TEXTURE': self._draw_downloading,
'EXCEPTION': self._draw_exception, 'EXCEPTION': self._draw_exception,
'PLEASE_SUBSCRIBE': self._draw_subscribe,
} }
if self._state in drawers: if self._state in drawers:
@@ -510,7 +601,7 @@ class BlenderCloudBrowser(bpy.types.Operator):
bgl.glColor4f(1.0, 1.0, 1.0, 1.0) bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
blf.size(font_id, 20, 72) blf.size(font_id, 20, 72)
blf.position(font_id, 5, 5, 0) blf.position(font_id, 5, 5, 0)
blf.draw(font_id, self._state) blf.draw(font_id, '%s %s' % (self._state, self.project_name))
bgl.glDisable(bgl.GL_BLEND) bgl.glDisable(bgl.GL_BLEND)
@staticmethod @staticmethod
@@ -641,6 +732,11 @@ class BlenderCloudBrowser(bpy.types.Operator):
blf.draw(font_id, line) blf.draw(font_id, line)
bgl.glDisable(bgl.GL_BLEND) bgl.glDisable(bgl.GL_BLEND)
def _draw_subscribe(self, context):
self._draw_text_on_colour(context,
'Click to subscribe to the Blender Cloud',
(0.0, 0.0, 0.2, 0.6))
def get_clicked(self) -> MenuItem: def get_clicked(self) -> MenuItem:
for item in self.current_display_content: for item in self.current_display_content:
@@ -652,11 +748,13 @@ class BlenderCloudBrowser(bpy.types.Operator):
def handle_item_selection(self, context, item: MenuItem): def handle_item_selection(self, context, item: MenuItem):
"""Called when the user clicks on a menu item that doesn't represent a folder.""" """Called when the user clicks on a menu item that doesn't represent a folder."""
from pillarsdk.utils import sanitize_filename
self.clear_images() self.clear_images()
self._state = 'DOWNLOADING_TEXTURE' self._state = 'DOWNLOADING_TEXTURE'
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 = [sanitize_filename(comp) for comp in 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)
@@ -689,6 +787,13 @@ class BlenderCloudBrowser(bpy.types.Operator):
future=signalling_future)) future=signalling_future))
self.async_task.add_done_callback(texture_download_completed) self.async_task.add_done_callback(texture_download_completed)
def open_browser_subscribe(self):
import webbrowser
webbrowser.open_new_tab('https://cloud.blender.org/join')
self.report({'INFO'}, 'We just started a browser for you.')
# store keymaps here to access after registration # store keymaps here to access after registration
addon_keymaps = [] addon_keymaps = []

View File

@@ -31,14 +31,15 @@ class UserNotLoggedInError(RuntimeError):
""" """
def __str__(self): def __str__(self):
return 'UserNotLoggedInError' return self.__class__.__name__
class CredentialsNotSyncedError(UserNotLoggedInError): class CredentialsNotSyncedError(UserNotLoggedInError):
"""Raised when the user may be logged in on Blender ID, but has no Blender Cloud token.""" """Raised when the user may be logged in on Blender ID, but has no Blender Cloud token."""
def __str__(self):
return 'CredentialsNotSyncedError' class NotSubscribedToCloudError(UserNotLoggedInError):
"""Raised when the user may be logged in on Blender ID, but has no Blender Cloud token."""
class PillarError(RuntimeError): class PillarError(RuntimeError):
@@ -62,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
@@ -71,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
@@ -171,11 +173,24 @@ async def check_pillar_credentials():
if not subclient: if not subclient:
raise CredentialsNotSyncedError() raise CredentialsNotSyncedError()
try: pillar_user_id = subclient['subclient_user_id']
await get_project_uuid('textures') # Any query will do. if not pillar_user_id:
except pillarsdk.UnauthorizedAccess:
raise CredentialsNotSyncedError() raise CredentialsNotSyncedError()
try:
db_user = await pillar_call(pillarsdk.User.find, pillar_user_id)
except (pillarsdk.UnauthorizedAccess, pillarsdk.ResourceNotFound):
raise CredentialsNotSyncedError()
roles = db_user.roles
log.debug('User has roles %r', roles)
if not roles or not {'subscriber', 'demo'}.intersection(set(roles)):
# Delete the subclient info. This forces a re-check later, which can
# then pick up on the user's new status.
del profile.subclients[SUBCLIENT_ID]
profile.save_json()
raise NotSubscribedToCloudError()
async def refresh_pillar_credentials(): async def refresh_pillar_credentials():
"""Refreshes the authentication token on Pillar. """Refreshes the authentication token on Pillar.
@@ -193,11 +208,15 @@ async def refresh_pillar_credentials():
# Create a subclient token and send it to Pillar. # Create a subclient token and send it to Pillar.
# May raise a blender_id.BlenderIdCommError # May raise a blender_id.BlenderIdCommError
try:
blender_id.create_subclient_token(SUBCLIENT_ID, pillar_endpoint) blender_id.create_subclient_token(SUBCLIENT_ID, pillar_endpoint)
except blender_id.communication.BlenderIdCommError as ex:
log.warning("Unable to create authentication token: %s", ex)
raise CredentialsNotSyncedError()
# Test the new URL # Test the new URL
_pillar_api = None _pillar_api = None
await get_project_uuid('textures') # Any query will do. await check_pillar_credentials()
async def get_project_uuid(project_url: str) -> str: async def get_project_uuid(project_url: str) -> str:
@@ -217,7 +236,7 @@ async def get_project_uuid(project_url: str) -> str:
async def get_nodes(project_uuid: str = None, parent_node_uuid: str = None, async def get_nodes(project_uuid: str = None, parent_node_uuid: str = None,
node_type: str = None) -> list: node_type = None) -> list:
"""Gets nodes for either a project or given a parent node. """Gets nodes for either a project or given a parent node.
@param project_uuid: the UUID of the project, or None if only querying by parent_node_uuid. @param project_uuid: the UUID of the project, or None if only querying by parent_node_uuid.
@@ -242,7 +261,10 @@ async def get_nodes(project_uuid: str = None, parent_node_uuid: str = None,
where['project'] = project_uuid where['project'] = project_uuid
if node_type: if node_type:
if isinstance(node_type, str):
where['node_type'] = node_type where['node_type'] = node_type
else:
where['node_type'] = {'$in': node_type}
children = await pillar_call(pillarsdk.Node.all, { children = await pillar_call(pillarsdk.Node.all, {
'projection': {'name': 1, 'parent': 1, 'node_type': 1, 'projection': {'name': 1, 'parent': 1, 'node_type': 1,
@@ -250,12 +272,24 @@ async def get_nodes(project_uuid: str = None, parent_node_uuid: str = None,
'properties.files': 1, 'properties.files': 1,
'properties.content_type': 1, 'picture': 1}, 'properties.content_type': 1, 'picture': 1},
'where': where, 'where': where,
'sort': 'properties.order',
'embed': ['parent']}) 'embed': ['parent']})
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_from_endpoint,
'/bcloud/texture-libraries')
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,
@@ -367,7 +401,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 {}"
@@ -439,8 +473,22 @@ async def download_texture_thumbnail(texture_node, desired_size: str,
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
# Find the File that belongs to this texture node # Find out which file to use for the thumbnail picture.
pic_uuid = texture_node['picture'] pic_uuid = texture_node.picture
if not pic_uuid:
# Fall back to the first texture file, if it exists.
log.debug('Node %r does not have a picture, falling back to first file.',
texture_node['_id'])
files = texture_node.properties and texture_node.properties.files
if not files:
log.info('Node %r does not have a picture nor files, skipping.', texture_node['_id'])
return
pic_uuid = files[0].file
if not pic_uuid:
log.info('Node %r does not have a picture nor files, skipping.', texture_node['_id'])
return
# Load the File that belongs to this texture node's picture.
loop.call_soon_threadsafe(thumbnail_loading, texture_node, texture_node) loop.call_soon_threadsafe(thumbnail_loading, texture_node, texture_node)
file_desc = await pillar_call(pillarsdk.File.find, pic_uuid, params={ file_desc = await pillar_call(pillarsdk.File.find, pic_uuid, params={
'projection': {'filename': 1, 'variations': 1, 'width': 1, 'height': 1}, 'projection': {'filename': 1, 'variations': 1, 'width': 1, 'height': 1},
@@ -495,8 +543,13 @@ async def download_file_by_uuid(file_uuid,
metadata_file = os.path.join(metadata_directory, 'files', '%s.json' % file_uuid) metadata_file = os.path.join(metadata_directory, 'files', '%s.json' % file_uuid)
save_as_json(file_desc, metadata_file) save_as_json(file_desc, metadata_file)
file_path = os.path.join(target_directory, root, ext = os.path.splitext(file_desc['filename'])
sanitize_filename('%s-%s' % (map_type, file_desc['filename']))) if root.endswith(map_type):
target_filename = '%s%s' % (root, ext)
else:
target_filename = '%s-%s%s' % (root, map_type, ext)
file_path = os.path.join(target_directory, sanitize_filename(target_filename))
file_url = file_desc['link'] file_url = file_desc['link']
# log.debug('Texture %r:\n%s', file_uuid, pprint.pformat(file_desc.to_dict())) # log.debug('Texture %r:\n%s', file_uuid, pprint.pformat(file_desc.to_dict()))
loop.call_soon_threadsafe(file_loading, file_path, file_desc) loop.call_soon_threadsafe(file_loading, file_path, file_desc)

View File

@@ -18,8 +18,9 @@ def load_wheel(module_name, fname_prefix):
try: try:
module = __import__(module_name) module = __import__(module_name)
except ImportError: except ImportError as ex:
pass log.debug('Unable to import %s directly, will try wheel: %s',
module_name, ex)
else: else:
log.debug('Was able to load %s from %s, no need to load wheel %s', log.debug('Was able to load %s from %s, no need to load wheel %s',
module_name, module.__file__, fname_prefix) module_name, module.__file__, fname_prefix)

View File

@@ -1,7 +1,7 @@
# Primary requirements: # Primary requirements:
CacheControl==0.11.6 CacheControl==0.11.6
lockfile==0.12.2 lockfile==0.12.2
pillarsdk==1.0.0 pillarsdk==1.2.0
wheel==0.29.0 wheel==0.29.0
# Secondary requirements: # Secondary requirements:

View File

@@ -173,7 +173,7 @@ setup(
'wheels': BuildWheels}, 'wheels': BuildWheels},
name='blender_cloud', name='blender_cloud',
description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.', description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.',
version='1.0.0', version='1.1.0',
author='Sybren A. Stüvel', author='Sybren A. Stüvel',
author_email='sybren@stuvel.eu', author_email='sybren@stuvel.eu',
packages=find_packages('.'), packages=find_packages('.'),