Compare commits

...

9 Commits

Author SHA1 Message Date
40732e0487 Updated changelog 2019-01-04 11:13:32 +01:00
b86bffbdbb Bumped version to 1.11.0 2019-01-04 11:12:36 +01:00
67f9d40fd3 Blender Sync: fixed missing icon in Blender 2.80
I like the 'DOTSDOWN' icon better, so I keep using it in Blender ≤ 2.79.
2019-01-04 11:09:20 +01:00
c4de4e9990 Fixed some MyPy warnings
This includes using `''` instead of `None` in some cases where an empty
string conveys 'nothing' equally well as `None`; in such cases keeping the
type the same rather than switching to another type is preferred.
2019-01-03 12:07:05 +01:00
6d2e6efa13 Update users of the material after replacing a HDRi
This causes a refresh and immediately shows the new texture in the viewport.
2019-01-03 11:33:19 +01:00
ff9ae0117d Fixed race condition referring to self when operator may have stopped running
The `file_loading` function is called deferred by asyncio, and can thus
be called when the operator has already stopped loading. This is fixed by
not referring to `self` in that function, and taking the logger from the
outer scope.
2019-01-03 11:32:40 +01:00
974d33e3a3 Texture Browser updated for Blender 2.8 drawing
The drawing code has been abstracted into a `draw.py` for Blender 2.8
and `draw_27.py` for earlier versions.
2019-01-03 10:41:42 +01:00
8de3a0bba2 Moved texture browser to its own module
This places it in the same kind of structure as Attract and Flamenco.
2019-01-02 16:47:33 +01:00
6f705b917f Removed local import 2019-01-02 16:47:11 +01:00
18 changed files with 520 additions and 331 deletions

View File

@@ -1,6 +1,12 @@
# Blender Cloud changelog # Blender Cloud changelog
## Version 1.11.0 (2019-01-04)
- Texture Browser now works on Blender 2.8.
- Blender Sync: Fixed compatibility issue with Blender 2.8.
## Version 1.10.0 (2019-01-02) ## Version 1.10.0 (2019-01-02)
- Bundles Blender-Asset-Tracer 0.8. - Bundles Blender-Asset-Tracer 0.8.

View File

@@ -21,7 +21,7 @@
bl_info = { bl_info = {
'name': 'Blender Cloud', 'name': 'Blender Cloud',
"author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis", "author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis",
'version': (1, 10, 0), 'version': (1, 11, 0),
'blender': (2, 80, 0), 'blender': (2, 80, 0),
'location': 'Addon Preferences panel, and Ctrl+Shift+Alt+A anywhere for texture browser', 'location': 'Addon Preferences panel, and Ctrl+Shift+Alt+A anywhere for texture browser',
'description': 'Texture library browser and Blender Sync. Requires the Blender ID addon ' 'description': 'Texture library browser and Blender Sync. Requires the Blender ID addon '

View File

@@ -511,7 +511,7 @@ if system == "win32":
_get_win_folder = _get_win_folder_with_pywin32 _get_win_folder = _get_win_folder_with_pywin32
except ImportError: except ImportError:
try: try:
from ctypes import windll from ctypes import windll # type: ignore
_get_win_folder = _get_win_folder_with_ctypes _get_win_folder = _get_win_folder_with_ctypes
except ImportError: except ImportError:
try: try:

View File

@@ -23,6 +23,7 @@ import traceback
import concurrent.futures import concurrent.futures
import logging import logging
import gc import gc
import typing
import bpy import bpy
@@ -238,7 +239,7 @@ class AsyncModalOperatorMixin:
self._stop_async_task() self._stop_async_task()
context.window_manager.event_timer_remove(self.timer) context.window_manager.event_timer_remove(self.timer)
def _new_async_task(self, async_task: asyncio.coroutine, future: asyncio.Future = None): def _new_async_task(self, async_task: typing.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)

View File

@@ -40,6 +40,11 @@ ADDON_NAME = 'blender_cloud'
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
icons = None icons = None
if bpy.app.version < (2, 80):
SYNC_SELECT_VERSION_ICON = 'DOTSDOWN'
else:
SYNC_SELECT_VERSION_ICON = 'DOWNARROW_HLT'
@functools.lru_cache() @functools.lru_cache()
def factor(factor: float) -> dict: def factor(factor: float) -> dict:
@@ -385,7 +390,7 @@ class BlenderCloudPreferences(AddonPreferences):
props.blender_version = version props.blender_version = version
row_pull.operator('pillar.sync', row_pull.operator('pillar.sync',
text='', text='',
icon='DOTSDOWN').action = 'SELECT' icon=SYNC_SELECT_VERSION_ICON).action = 'SELECT'
else: else:
row_pull.label(text='Cloud Sync is running.') row_pull.label(text='Cloud Sync is running.')

View File

@@ -445,7 +445,8 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
return filepath return filepath
async def bat_pack(self, filepath: Path) -> (Path, typing.Optional[Path], typing.List[Path]): async def bat_pack(self, filepath: Path) \
-> typing.Tuple[Path, typing.Optional[Path], typing.List[Path]]:
"""BAT-packs the blendfile to the destination directory. """BAT-packs the blendfile to the destination directory.
Returns the path of the destination blend file. Returns the path of the destination blend file.

View File

@@ -96,18 +96,18 @@ class CloudPath(pathlib.PurePosixPath):
def project_uuid(self) -> str: def project_uuid(self) -> str:
assert self.parts[0] == '/' assert self.parts[0] == '/'
if len(self.parts) <= 1: if len(self.parts) <= 1:
return None return ''
return self.parts[1] return self.parts[1]
@property @property
def node_uuids(self) -> list: def node_uuids(self) -> tuple:
assert self.parts[0] == '/' assert self.parts[0] == '/'
return self.parts[2:] return self.parts[2:]
@property @property
def node_uuid(self) -> str: def node_uuid(self) -> str:
if len(self.parts) <= 2: if len(self.parts) <= 2:
return None return ''
return self.parts[-1] return self.parts[-1]

View File

@@ -25,6 +25,7 @@ import functools
import logging import logging
import pathlib import pathlib
import tempfile import tempfile
import typing
import shutil import shutil
import bpy import bpy
@@ -100,7 +101,7 @@ async def find_sync_group_id(home_project_id: str,
user_id: str, user_id: str,
blender_version: str, blender_version: str,
*, *,
may_create=True) -> str: may_create=True) -> typing.Tuple[str, str]:
"""Finds the group node in which to store sync assets. """Finds the group node in which to store sync assets.
If the group node doesn't exist and may_create=True, it creates it. If the group node doesn't exist and may_create=True, it creates it.
@@ -122,7 +123,7 @@ async def find_sync_group_id(home_project_id: str,
if not may_create and sync_group is None: if not may_create and sync_group is None:
log.info("Sync folder doesn't exist, and not creating it either.") log.info("Sync folder doesn't exist, and not creating it either.")
return None, None return '', ''
# Find/create the sub-group for the requested Blender version # Find/create the sub-group for the requested Blender version
try: try:
@@ -144,7 +145,7 @@ async def find_sync_group_id(home_project_id: str,
if not may_create and sub_sync_group is None: if not may_create and sub_sync_group is None:
log.info("Sync folder for Blender version %s doesn't exist, " log.info("Sync folder for Blender version %s doesn't exist, "
"and not creating it either.", blender_version) "and not creating it either.", blender_version)
return sync_group['_id'], None return sync_group['_id'], ''
return sync_group['_id'], sub_sync_group['_id'] return sync_group['_id'], sub_sync_group['_id']
@@ -203,9 +204,9 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin,
bl_description = 'Synchronises Blender settings with Blender Cloud' bl_description = 'Synchronises Blender settings with Blender Cloud'
log = logging.getLogger('bpy.ops.%s' % bl_idname) log = logging.getLogger('bpy.ops.%s' % bl_idname)
home_project_id = None home_project_id = ''
sync_group_id = None # top-level sync group node ID sync_group_id = '' # top-level sync group node ID
sync_group_versioned_id = None # sync group node ID for the given Blender version. sync_group_versioned_id = '' # sync group node ID for the given Blender version.
action = bpy.props.EnumProperty( action = bpy.props.EnumProperty(
items=[ items=[
@@ -387,12 +388,12 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin,
"""Loads files from the Pillar server.""" """Loads files from the Pillar server."""
# If the sync group node doesn't exist, offer a list of groups that do. # If the sync group node doesn't exist, offer a list of groups that do.
if self.sync_group_id is None: if not self.sync_group_id:
self.bss_report({'ERROR'}, self.bss_report({'ERROR'},
'There are no synced Blender settings in your Blender Cloud.') 'There are no synced Blender settings in your Blender Cloud.')
return return
if self.sync_group_versioned_id is None: if not self.sync_group_versioned_id:
self.bss_report({'ERROR'}, 'Therre are no synced Blender settings for version %s' % self.bss_report({'ERROR'}, 'Therre are no synced Blender settings for version %s' %
self.blender_version) self.blender_version)
return return

View File

@@ -18,249 +18,35 @@
import asyncio import asyncio
import logging import logging
import threading
import os import os
import threading
import typing
import bpy import bpy
import bgl import bgl
import blf
import pillarsdk import pillarsdk
from . import async_loop, pillar, cache, blender, utils from .. import async_loop, pillar, cache, blender, utils
from . import menu_item as menu_item_mod # so that we can have menu items called 'menu_item'
from . import nodes
if bpy.app.version < (2, 80):
from . import draw_27 as draw
else:
from . import draw
REQUIRED_ROLES_FOR_TEXTURE_BROWSER = {'subscriber', 'demo'} REQUIRED_ROLES_FOR_TEXTURE_BROWSER = {'subscriber', 'demo'}
MOUSE_SCROLL_PIXELS_PER_TICK = 50 MOUSE_SCROLL_PIXELS_PER_TICK = 50
ICON_WIDTH = 128
ICON_HEIGHT = 128
TARGET_ITEM_WIDTH = 400 TARGET_ITEM_WIDTH = 400
TARGET_ITEM_HEIGHT = 128 TARGET_ITEM_HEIGHT = 128
ITEM_MARGIN_X = 5 ITEM_MARGIN_X = 5
ITEM_MARGIN_Y = 5 ITEM_MARGIN_Y = 5
ITEM_PADDING_X = 5 ITEM_PADDING_X = 5
library_path = '/tmp'
library_icons_path = os.path.join(os.path.dirname(__file__), "icons")
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class SpecialFolderNode(pillarsdk.Node):
NODE_TYPE = 'SPECIAL'
class UpNode(SpecialFolderNode):
NODE_TYPE = 'UP'
def __init__(self):
super().__init__()
self['_id'] = 'UP'
self['node_type'] = self.NODE_TYPE
class ProjectNode(SpecialFolderNode):
NODE_TYPE = 'PROJECT'
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'] = self.NODE_TYPE
class MenuItem:
"""GUI menu item for the 3D View GUI."""
icon_margin_x = 4
icon_margin_y = 4
text_margin_x = 6
text_size = 12
text_size_small = 10
DEFAULT_ICONS = {
'FOLDER': os.path.join(library_icons_path, 'folder.png'),
'SPINNER': os.path.join(library_icons_path, 'spinner.png'),
'ERROR': os.path.join(library_icons_path, 'error.png'),
}
FOLDER_NODE_TYPES = {'group_texture', 'group_hdri', UpNode.NODE_TYPE, ProjectNode.NODE_TYPE}
SUPPORTED_NODE_TYPES = {'texture', 'hdri'}.union(FOLDER_NODE_TYPES)
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:
self.log.info('Invalid node type in node: %s', node)
raise TypeError('Node of type %r not supported; supported are %r.' % (
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.file_desc = file_desc # pillarsdk.File object, or None if a 'folder' node.
self.label_text = label_text
self.small_text = self._small_text_from_node()
self._thumb_path = ''
self.icon = None
self._is_folder = node['node_type'] in self.FOLDER_NODE_TYPES
self._is_spinning = False
# 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
# Updated when drawing the image
self.x = 0
self.y = 0
self.width = 0
self.height = 0
def _small_text_from_node(self) -> str:
"""Return the components of the texture (i.e. which map types are available)."""
if not self.node:
return ''
try:
node_files = self.node.properties.files
except AttributeError:
# Happens for nodes that don't have .properties.files.
return ''
if not node_files:
return ''
map_types = {f.map_type for f in node_files if f.map_type}
map_types.discard('color') # all textures have colour
if not map_types:
return ''
return ', '.join(sorted(map_types))
def sort_key(self):
"""Key for sorting lists of MenuItems."""
return self._order, self.label_text
@property
def thumb_path(self) -> str:
return self._thumb_path
@thumb_path.setter
def thumb_path(self, new_thumb_path: str):
self._is_spinning = new_thumb_path == 'SPINNER'
self._thumb_path = self.DEFAULT_ICONS.get(new_thumb_path, new_thumb_path)
if self._thumb_path:
self.icon = bpy.data.images.load(filepath=self._thumb_path)
else:
self.icon = None
@property
def node_uuid(self) -> str:
return self.node['_id']
def represents(self, node) -> bool:
"""Returns True iff this MenuItem represents the given node."""
node_uuid = node['_id']
return self.node_uuid == node_uuid
def update(self, node, file_desc, thumb_path: str, label_text=None):
# We can get updated information about our Node, but a MenuItem should
# always represent one node, and it shouldn't be shared between nodes.
if self.node_uuid != node['_id']:
raise ValueError("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.thumb_path = thumb_path
if label_text is not None:
self.label_text = label_text
if thumb_path == 'ERROR':
self.small_text = 'This open is broken'
else:
self.small_text = self._small_text_from_node()
@property
def is_folder(self) -> bool:
return self._is_folder
@property
def is_spinning(self) -> bool:
return self._is_spinning
def update_placement(self, x, y, width, height):
"""Use OpenGL to draw this one menu item."""
self.x = x
self.y = y
self.width = width
self.height = height
def draw(self, highlighted: bool):
bgl.glEnable(bgl.GL_BLEND)
if highlighted:
bgl.glColor4f(0.555, 0.555, 0.555, 0.8)
else:
bgl.glColor4f(0.447, 0.447, 0.447, 0.8)
bgl.glRectf(self.x, self.y, self.x + self.width, self.y + self.height)
texture = self.icon
if texture:
err = texture.gl_load(filter=bgl.GL_NEAREST, mag=bgl.GL_NEAREST)
assert not err, 'OpenGL error: %i' % err
bgl.glColor4f(0.0, 0.0, 1.0, 0.5)
# bgl.glLineWidth(1.5)
# ------ TEXTURE ---------#
if texture:
bgl.glBindTexture(bgl.GL_TEXTURE_2D, texture.bindcode[0])
bgl.glEnable(bgl.GL_TEXTURE_2D)
bgl.glBlendFunc(bgl.GL_SRC_ALPHA, bgl.GL_ONE_MINUS_SRC_ALPHA)
bgl.glColor4f(1, 1, 1, 1)
bgl.glBegin(bgl.GL_QUADS)
bgl.glTexCoord2d(0, 0)
bgl.glVertex2d(self.x + self.icon_margin_x, self.y)
bgl.glTexCoord2d(0, 1)
bgl.glVertex2d(self.x + self.icon_margin_x, self.y + ICON_HEIGHT)
bgl.glTexCoord2d(1, 1)
bgl.glVertex2d(self.x + self.icon_margin_x + ICON_WIDTH, self.y + ICON_HEIGHT)
bgl.glTexCoord2d(1, 0)
bgl.glVertex2d(self.x + self.icon_margin_x + ICON_WIDTH, self.y)
bgl.glEnd()
bgl.glDisable(bgl.GL_TEXTURE_2D)
bgl.glDisable(bgl.GL_BLEND)
if texture:
texture.gl_free()
# draw some text
font_id = 0
text_dpi = blender.ctx_preferences().system.dpi
text_x = self.x + self.icon_margin_x + ICON_WIDTH + self.text_margin_x
text_y = self.y + ICON_HEIGHT * 0.5 - 0.25 * self.text_size
blf.position(font_id, text_x, text_y, 0)
blf.size(font_id, self.text_size, text_dpi)
blf.draw(font_id, self.label_text)
# draw the small text
bgl.glColor4f(1.0, 1.0, 1.0, 0.5)
blf.size(font_id, self.text_size_small, text_dpi)
blf.position(font_id, text_x, self.y + 0.5 * self.text_size_small, 0)
blf.draw(font_id, self.small_text)
def hits(self, mouse_x: int, mouse_y: int) -> bool:
return self.x < mouse_x < self.x + self.width and self.y < mouse_y < self.y + self.height
class BlenderCloudBrowser(pillar.PillarOperatorMixin, class BlenderCloudBrowser(pillar.PillarOperatorMixin,
async_loop.AsyncModalOperatorMixin, async_loop.AsyncModalOperatorMixin,
bpy.types.Operator): bpy.types.Operator):
@@ -273,17 +59,17 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
project_name = '' project_name = ''
# 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.
path_stack = [] path_stack = [] # type: typing.List[pillarsdk.Node]
# This contains a stack of MenuItem objects that lead up to the currently browsed node. # This contains a stack of MenuItem objects that lead up to the currently browsed node.
menu_item_stack = [] menu_item_stack = [] # type: typing.List[menu_item_mod.MenuItem]
timer = None timer = None
log = logging.getLogger('%s.BlenderCloudBrowser' % __name__) log = logging.getLogger('%s.BlenderCloudBrowser' % __name__)
_menu_item_lock = threading.Lock() _menu_item_lock = threading.Lock()
current_display_content = [] # list of MenuItems currently displayed current_display_content = [] # type: typing.List[menu_item_mod.MenuItem]
loaded_images = set() loaded_images = set() # type: typing.Set[str]
thumbnails_cache = '' thumbnails_cache = ''
maximized_area = False maximized_area = False
@@ -422,7 +208,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
bpy.context.window.cursor_set('HAND') bpy.context.window.cursor_set('HAND')
def descend_node(self, menu_item: MenuItem): def descend_node(self, menu_item: menu_item_mod.MenuItem):
"""Descends the node hierarchy by visiting this menu item's node. """Descends the node hierarchy by visiting this menu item's 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.
@@ -431,7 +217,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
node = menu_item.node node = menu_item.node
assert isinstance(node, pillarsdk.Node), 'Wrong type %s' % node assert isinstance(node, pillarsdk.Node), 'Wrong type %s' % node
if isinstance(node, UpNode): if isinstance(node, nodes.UpNode):
# Going up. # Going up.
self.log.debug('Going up to %r', self.current_path) self.log.debug('Going up to %r', self.current_path)
self.current_path = self.current_path.parent self.current_path = self.current_path.parent
@@ -443,7 +229,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
self.project_name = '' self.project_name = ''
else: else:
# Going down, keep track of where we were # Going down, keep track of where we were
if isinstance(node, ProjectNode): if isinstance(node, nodes.ProjectNode):
self.project_name = node['name'] self.project_name = node['name']
self.current_path /= node['_id'] self.current_path /= node['_id']
@@ -486,13 +272,14 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
self.loaded_images.clear() self.loaded_images.clear()
self.current_display_content.clear() self.current_display_content.clear()
def add_menu_item(self, *args) -> MenuItem: def add_menu_item(self, *args) -> menu_item_mod.MenuItem:
menu_item = MenuItem(*args) menu_item = menu_item_mod.MenuItem(*args)
# 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:
self.current_display_content.append(menu_item) self.current_display_content.append(menu_item)
self.loaded_images.add(menu_item.icon.filepath_raw) if menu_item.icon is not None:
self.loaded_images.add(menu_item.icon.filepath_raw)
self.sort_menu() self.sort_menu()
@@ -520,7 +307,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
return return
with self._menu_item_lock: with self._menu_item_lock:
self.current_display_content.sort(key=MenuItem.sort_key) self.current_display_content.sort(key=menu_item_mod.MenuItem.sort_key)
async def async_download_previews(self): async def async_download_previews(self):
self._state = 'BROWSING' self._state = 'BROWSING'
@@ -550,17 +337,17 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
self.log.debug('No node UUID and no project UUID, listing available projects') self.log.debug('No node UUID and no project UUID, listing available projects')
children = await pillar.get_texture_projects() children = await pillar.get_texture_projects()
for proj_dict in children: for proj_dict in children:
self.add_menu_item(ProjectNode(proj_dict), None, 'FOLDER', proj_dict['name']) self.add_menu_item(nodes.ProjectNode(proj_dict), None, 'FOLDER', proj_dict['name'])
return return
# Make sure we can go up again. # Make sure we can go up again.
self.add_menu_item(UpNode(), None, 'FOLDER', '.. up ..') self.add_menu_item(nodes.UpNode(), None, 'FOLDER', '.. up ..')
# Download all child nodes # Download all child nodes
self.log.debug('Iterating over child nodes of %r', self.current_path) 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: if child['node_type'] not in menu_item_mod.MenuItem.SUPPORTED_NODE_TYPES:
self.log.debug('Skipping node of type %r', child['node_type']) self.log.debug('Skipping node of type %r', child['node_type'])
continue continue
self.add_menu_item(child, None, 'FOLDER', child['name']) self.add_menu_item(child, None, 'FOLDER', child['name'])
@@ -610,12 +397,9 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
drawer(context) drawer(context)
# For debugging: draw the state # For debugging: draw the state
font_id = 0 draw.text((5, 5),
bgl.glColor4f(1.0, 1.0, 1.0, 1.0) '%s %s' % (self._state, self.project_name),
blf.size(font_id, 20, 72) rgba=(1.0, 1.0, 1.0, 1.0), fsize=12)
blf.position(font_id, 5, 5, 0)
blf.draw(font_id, '%s %s' % (self._state, self.project_name))
bgl.glDisable(bgl.GL_BLEND)
@staticmethod @staticmethod
def _window_region(context): def _window_region(context):
@@ -626,6 +410,12 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
def _draw_browser(self, context): def _draw_browser(self, context):
"""OpenGL drawing code for the BROWSING state.""" """OpenGL drawing code for the BROWSING state."""
from . import draw
if not self.current_display_content:
self._draw_text_on_colour(context, "Communicating with Blender Cloud",
(0.0, 0.0, 0.0, 0.6))
return
window_region = self._window_region(context) window_region = self._window_region(context)
content_width = window_region.width - ITEM_MARGIN_X * 2 content_width = window_region.width - ITEM_MARGIN_X * 2
@@ -643,46 +433,33 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
block_height = item_height + ITEM_MARGIN_Y block_height = item_height + ITEM_MARGIN_Y
bgl.glEnable(bgl.GL_BLEND) bgl.glEnable(bgl.GL_BLEND)
bgl.glColor4f(0.0, 0.0, 0.0, 0.6) draw.aabox((0, 0), (window_region.width, window_region.height),
bgl.glRectf(0, 0, window_region.width, window_region.height) (0.0, 0.0, 0.0, 0.6))
if self.current_display_content: bottom_y = float('inf')
bottom_y = float('inf')
# The -1 / +2 are for extra rows that are drawn only half at the top/bottom. # The -1 / +2 are for extra rows that are drawn only half at the top/bottom.
first_item_idx = max(0, int(-self.scroll_offset // block_height - 1) * col_count) first_item_idx = max(0, int(-self.scroll_offset // block_height - 1) * col_count)
items_per_page = int(content_height // item_height + 2) * col_count items_per_page = int(content_height // item_height + 2) * col_count
last_item_idx = first_item_idx + items_per_page last_item_idx = first_item_idx + items_per_page
for item_idx, item in enumerate(self.current_display_content): for item_idx, item in enumerate(self.current_display_content):
x = content_x + (item_idx % col_count) * block_width x = content_x + (item_idx % col_count) * block_width
y = content_y - (item_idx // col_count) * block_height - self.scroll_offset y = content_y - (item_idx // col_count) * block_height - self.scroll_offset
item.update_placement(x, y, item_width, item_height) item.update_placement(x, y, item_width, item_height)
if first_item_idx <= item_idx < last_item_idx: if first_item_idx <= item_idx < last_item_idx:
# Only draw if the item is actually on screen. # Only draw if the item is actually on screen.
item.draw(highlighted=item.hits(self.mouse_x, self.mouse_y)) item.draw(highlighted=item.hits(self.mouse_x, self.mouse_y))
bottom_y = min(y, bottom_y) bottom_y = min(y, bottom_y)
self.scroll_offset_space_left = window_region.height - bottom_y self.scroll_offset_space_left = window_region.height - bottom_y
self.scroll_offset_max = (self.scroll_offset - self.scroll_offset_max = (self.scroll_offset -
self.scroll_offset_space_left + self.scroll_offset_space_left +
0.25 * block_height) 0.25 * block_height)
else:
font_id = 0
text = "Communicating with Blender Cloud"
bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
blf.size(font_id, 20, 72)
text_width, text_height = blf.dimensions(font_id, text)
blf.position(font_id,
content_x + content_width * 0.5 - text_width * 0.5,
content_y - content_height * 0.3 + text_height * 0.5, 0)
blf.draw(font_id, text)
bgl.glDisable(bgl.GL_BLEND) bgl.glDisable(bgl.GL_BLEND)
# bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
def _draw_downloading(self, context): def _draw_downloading(self, context):
"""OpenGL drawing code for the DOWNLOADING_TEXTURE state.""" """OpenGL drawing code for the DOWNLOADING_TEXTURE state."""
@@ -705,21 +482,15 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
'Initializing', 'Initializing',
(0.0, 0.0, 0.2, 0.6)) (0.0, 0.0, 0.2, 0.6))
def _draw_text_on_colour(self, context, text, bgcolour): def _draw_text_on_colour(self, context, text: str, bgcolour):
content_height, content_width = self._window_size(context) content_height, content_width = self._window_size(context)
bgl.glEnable(bgl.GL_BLEND) bgl.glEnable(bgl.GL_BLEND)
bgl.glColor4f(*bgcolour)
bgl.glRectf(0, 0, content_width, content_height)
font_id = 0 draw.aabox((0, 0), (content_width, content_height), bgcolour)
bgl.glColor4f(1.0, 1.0, 1.0, 1.0) draw.text((content_width * 0.5, content_height * 0.7),
blf.size(font_id, 20, 72) text, fsize=20, align='C')
text_width, text_height = blf.dimensions(font_id, text)
blf.position(font_id,
content_width * 0.5 - text_width * 0.5,
content_height * 0.7 + text_height * 0.5, 0)
blf.draw(font_id, text)
bgl.glDisable(bgl.GL_BLEND) bgl.glDisable(bgl.GL_BLEND)
def _window_size(self, context): def _window_size(self, context):
@@ -736,10 +507,8 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
content_height, content_width = self._window_size(context) content_height, content_width = self._window_size(context)
bgl.glEnable(bgl.GL_BLEND) bgl.glEnable(bgl.GL_BLEND)
bgl.glColor4f(0.2, 0.0, 0.0, 0.6) draw.aabox((0, 0), (content_width, content_height), (0.2, 0.0, 0.0, 0.6))
bgl.glRectf(0, 0, content_width, content_height)
font_id = 0
ex = self.async_task.exception() ex = self.async_task.exception()
if isinstance(ex, pillar.UserNotLoggedInError): if isinstance(ex, pillar.UserNotLoggedInError):
ex_msg = 'You are not logged in on Blender ID. Please log in at User Preferences, ' \ ex_msg = 'You are not logged in on Blender ID. Please log in at User Preferences, ' \
@@ -749,20 +518,10 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
if not ex_msg: if not ex_msg:
ex_msg = str(type(ex)) ex_msg = str(type(ex))
text = "An error occurred:\n%s" % ex_msg text = "An error occurred:\n%s" % ex_msg
lines = textwrap.wrap(text) lines = textwrap.wrap(text, width=100)
bgl.glColor4f(1.0, 1.0, 1.0, 1.0) draw.text((content_width * 0.1, content_height * 0.9), lines, fsize=16)
blf.size(font_id, 20, 72)
_, text_height = blf.dimensions(font_id, 'yhBp')
def position(line_nr):
blf.position(font_id,
content_width * 0.1,
content_height * 0.8 - line_nr * text_height, 0)
for line_idx, line in enumerate(lines):
position(line_idx)
blf.draw(font_id, line)
bgl.glDisable(bgl.GL_BLEND) bgl.glDisable(bgl.GL_BLEND)
def _draw_subscribe(self, context): def _draw_subscribe(self, context):
@@ -775,7 +534,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
'Click to renew your Blender Cloud subscription', 'Click to renew your Blender Cloud subscription',
(0.0, 0.0, 0.2, 0.6)) (0.0, 0.0, 0.2, 0.6))
def get_clicked(self) -> MenuItem: def get_clicked(self) -> typing.Optional[menu_item_mod.MenuItem]:
for item in self.current_display_content: for item in self.current_display_content:
if item.hits(self.mouse_x, self.mouse_y): if item.hits(self.mouse_x, self.mouse_y):
@@ -783,7 +542,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
return None return None
def handle_item_selection(self, context, item: MenuItem): def handle_item_selection(self, context, item: menu_item_mod.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 from pillarsdk.utils import sanitize_filename
@@ -935,13 +694,11 @@ class PILLAR_OT_switch_hdri(pillar.PillarOperatorMixin,
self._state = 'QUIT' self._state = 'QUIT'
async def download_and_replace(self, context): async def download_and_replace(self, context):
from .pillar import sanitize_filename
self._state = 'DOWNLOADING_TEXTURE' self._state = 'DOWNLOADING_TEXTURE'
current_image = bpy.data.images[self.image_name] current_image = bpy.data.images[self.image_name]
node = current_image['bcloud_node'] node = current_image['bcloud_node']
filename = '%s.taken_from_file' % sanitize_filename(node['name']) filename = '%s.taken_from_file' % pillar.sanitize_filename(node['name'])
local_path = os.path.dirname(bpy.path.abspath(current_image.filepath)) local_path = os.path.dirname(bpy.path.abspath(current_image.filepath))
top_texture_directory = bpy.path.abspath(context.scene.local_texture_dir) top_texture_directory = bpy.path.abspath(context.scene.local_texture_dir)
@@ -951,21 +708,27 @@ class PILLAR_OT_switch_hdri(pillar.PillarOperatorMixin,
resolution = next(file_ref['resolution'] for file_ref in node['properties']['files'] resolution = next(file_ref['resolution'] for file_ref in node['properties']['files']
if file_ref['file'] == file_uuid) if file_ref['file'] == file_uuid)
self.log.info('Downloading file %r-%s to %s', file_uuid, resolution, local_path) my_log = self.log
self.log.debug('Metadata will be stored at %s', meta_path) my_log.info('Downloading file %r-%s to %s', file_uuid, resolution, local_path)
my_log.debug('Metadata will be stored at %s', meta_path)
def file_loading(file_path, file_desc, map_type): def file_loading(file_path, file_desc, map_type):
self.log.info('Texture downloading to %s (%s)', my_log.info('Texture downloading to %s (%s)',
file_path, utils.sizeof_fmt(file_desc['length'])) file_path, utils.sizeof_fmt(file_desc['length']))
async def file_loaded(file_path, file_desc, map_type): async def file_loaded(file_path, file_desc, map_type):
if context.scene.local_texture_dir.startswith('//'): if context.scene.local_texture_dir.startswith('//'):
file_path = bpy.path.relpath(file_path) file_path = bpy.path.relpath(file_path)
self.log.info('Texture downloaded to %s', file_path) my_log.info('Texture downloaded to %s', file_path)
current_image['bcloud_file_uuid'] = file_uuid current_image['bcloud_file_uuid'] = file_uuid
current_image.filepath = file_path # This automatically reloads the image from disk. current_image.filepath = file_path # This automatically reloads the image from disk.
# This forces users of the image to update.
for datablocks in bpy.data.user_map({current_image}).values():
for datablock in datablocks:
datablock.update_tag()
await pillar.download_file_by_uuid(file_uuid, await pillar.download_file_by_uuid(file_uuid,
local_path, local_path,
meta_path, meta_path,

View File

@@ -0,0 +1,103 @@
"""OpenGL drawing code for the texture browser.
Requires Blender 2.80 or newer.
"""
import typing
import bgl
import blf
import bpy
import gpu
from gpu_extras.batch import batch_for_shader
shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
texture_shader = gpu.shader.from_builtin('2D_IMAGE')
Float2 = typing.Tuple[float, float]
Float3 = typing.Tuple[float, float, float]
Float4 = typing.Tuple[float, float, float, float]
def text(pos2d: Float2, display_text: typing.Union[str, typing.List[str]],
rgba: Float4 = (1.0, 1.0, 1.0, 1.0),
fsize=12,
align='L'):
"""Draw text with the top-left corner at 'pos2d'."""
dpi = bpy.context.preferences.system.dpi
gap = 12
x_pos, y_pos = pos2d
font_id = 0
blf.size(font_id, fsize, dpi)
# Compute the height of one line.
mwidth, mheight = blf.dimensions(font_id, "Tp") # Use high and low letters.
mheight *= 1.5
# Split text into lines.
if isinstance(display_text, str):
mylines = display_text.split("\n")
else:
mylines = display_text
maxwidth = 0
maxheight = len(mylines) * mheight
for idx, line in enumerate(mylines):
text_width, text_height = blf.dimensions(font_id, line)
if align == 'C':
newx = x_pos - text_width / 2
elif align == 'R':
newx = x_pos - text_width - gap
else:
newx = x_pos
# Draw
blf.position(font_id, newx, y_pos - mheight * idx, 0)
blf.color(font_id, rgba[0], rgba[1], rgba[2], rgba[3])
blf.draw(font_id, " " + line)
# saves max width
if maxwidth < text_width:
maxwidth = text_width
return maxwidth, maxheight
def aabox(v1: Float2, v2: Float2, rgba: Float4):
"""Draw an axis-aligned box."""
coords = [
(v1[0], v1[1]),
(v1[0], v2[1]),
(v2[0], v2[1]),
(v2[0], v1[1]),
]
shader.bind()
shader.uniform_float("color", rgba)
batch = batch_for_shader(shader, 'TRI_FAN', {"pos": coords})
batch.draw(shader)
def aabox_with_texture(v1: Float2, v2: Float2):
"""Draw an axis-aligned box with a texture."""
coords = [
(v1[0], v1[1]),
(v1[0], v2[1]),
(v2[0], v2[1]),
(v2[0], v1[1]),
]
texture_shader.bind()
texture_shader.uniform_int("image", 0)
batch = batch_for_shader(texture_shader, 'TRI_FAN', {
"pos": coords,
"texCoord": ((0, 0), (0, 1), (1, 1), (1, 0)),
})
batch.draw(texture_shader)
def bind_texture(texture: bpy.types.Image):
"""Bind a Blender image to a GL texture slot."""
bgl.glActiveTexture(bgl.GL_TEXTURE0)
bgl.glBindTexture(bgl.GL_TEXTURE_2D, texture.bindcode)

View File

@@ -0,0 +1,90 @@
"""OpenGL drawing code for the texture browser.
Requires Blender 2.79 or older.
"""
import typing
import bgl
import blf
import bpy
Float2 = typing.Tuple[float, float]
Float3 = typing.Tuple[float, float, float]
Float4 = typing.Tuple[float, float, float, float]
def text(pos2d: Float2, display_text: typing.Union[str, typing.List[str]],
rgba: Float4 = (1.0, 1.0, 1.0, 1.0),
fsize=12,
align='L'):
"""Draw text with the top-left corner at 'pos2d'."""
dpi = bpy.context.user_preferences.system.dpi
gap = 12
x_pos, y_pos = pos2d
font_id = 0
blf.size(font_id, fsize, dpi)
# Compute the height of one line.
mwidth, mheight = blf.dimensions(font_id, "Tp") # Use high and low letters.
mheight *= 1.5
# Split text into lines.
if isinstance(display_text, str):
mylines = display_text.split("\n")
else:
mylines = display_text
maxwidth = 0
maxheight = len(mylines) * mheight
for idx, line in enumerate(mylines):
text_width, text_height = blf.dimensions(font_id, line)
if align == 'C':
newx = x_pos - text_width / 2
elif align == 'R':
newx = x_pos - text_width - gap
else:
newx = x_pos
# Draw
blf.position(font_id, newx, y_pos - mheight * idx, 0)
bgl.glColor4f(*rgba)
blf.draw(font_id, " " + line)
# saves max width
if maxwidth < text_width:
maxwidth = text_width
return maxwidth, maxheight
def aabox(v1: Float2, v2: Float2, rgba: Float4):
"""Draw an axis-aligned box."""
bgl.glColor4f(*rgba)
bgl.glRectf(*v1, *v2)
def aabox_with_texture(v1: Float2, v2: Float2):
"""Draw an axis-aligned box with a texture."""
bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
bgl.glEnable(bgl.GL_TEXTURE_2D)
bgl.glBegin(bgl.GL_QUADS)
bgl.glTexCoord2d(0, 0)
bgl.glVertex2d(v1[0], v1[1])
bgl.glTexCoord2d(0, 1)
bgl.glVertex2d(v1[0], v2[1])
bgl.glTexCoord2d(1, 1)
bgl.glVertex2d(v2[0], v2[1])
bgl.glTexCoord2d(1, 0)
bgl.glVertex2d(v2[0], v1[1])
bgl.glEnd()
bgl.glDisable(bgl.GL_TEXTURE_2D)
def bind_texture(texture: bpy.types.Image):
"""Bind a Blender image to a GL texture slot."""
bgl.glBindTexture(bgl.GL_TEXTURE_2D, texture.bindcode[0])

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,192 @@
import logging
import os.path
import bpy
import bgl
import pillarsdk
from . import nodes
if bpy.app.version < (2, 80):
from . import draw_27 as draw
else:
from . import draw
library_icons_path = os.path.join(os.path.dirname(__file__), "icons")
ICON_WIDTH = 128
ICON_HEIGHT = 128
class MenuItem:
"""GUI menu item for the 3D View GUI."""
icon_margin_x = 4
icon_margin_y = 4
text_margin_x = 6
text_size = 12
text_size_small = 10
DEFAULT_ICONS = {
'FOLDER': os.path.join(library_icons_path, 'folder.png'),
'SPINNER': os.path.join(library_icons_path, 'spinner.png'),
'ERROR': os.path.join(library_icons_path, 'error.png'),
}
FOLDER_NODE_TYPES = {'group_texture', 'group_hdri',
nodes.UpNode.NODE_TYPE, nodes.ProjectNode.NODE_TYPE}
SUPPORTED_NODE_TYPES = {'texture', 'hdri'}.union(FOLDER_NODE_TYPES)
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:
self.log.info('Invalid node type in node: %s', node)
raise TypeError('Node of type %r not supported; supported are %r.' % (
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.file_desc = file_desc # pillarsdk.File object, or None if a 'folder' node.
self.label_text = label_text
self.small_text = self._small_text_from_node()
self._thumb_path = ''
self.icon = None
self._is_folder = node['node_type'] in self.FOLDER_NODE_TYPES
self._is_spinning = False
# 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
# Updated when drawing the image
self.x = 0
self.y = 0
self.width = 0
self.height = 0
def _small_text_from_node(self) -> str:
"""Return the components of the texture (i.e. which map types are available)."""
if not self.node:
return ''
try:
node_files = self.node.properties.files
except AttributeError:
# Happens for nodes that don't have .properties.files.
return ''
if not node_files:
return ''
map_types = {f.map_type for f in node_files if f.map_type}
map_types.discard('color') # all textures have colour
if not map_types:
return ''
return ', '.join(sorted(map_types))
def sort_key(self):
"""Key for sorting lists of MenuItems."""
return self._order, self.label_text
@property
def thumb_path(self) -> str:
return self._thumb_path
@thumb_path.setter
def thumb_path(self, new_thumb_path: str):
self._is_spinning = new_thumb_path == 'SPINNER'
self._thumb_path = self.DEFAULT_ICONS.get(new_thumb_path, new_thumb_path)
if self._thumb_path:
self.icon = bpy.data.images.load(filepath=self._thumb_path)
else:
self.icon = None
@property
def node_uuid(self) -> str:
return self.node['_id']
def represents(self, node) -> bool:
"""Returns True iff this MenuItem represents the given node."""
node_uuid = node['_id']
return self.node_uuid == node_uuid
def update(self, node, file_desc, thumb_path: str, label_text=None):
# We can get updated information about our Node, but a MenuItem should
# always represent one node, and it shouldn't be shared between nodes.
if self.node_uuid != node['_id']:
raise ValueError("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.thumb_path = thumb_path
if label_text is not None:
self.label_text = label_text
if thumb_path == 'ERROR':
self.small_text = 'This open is broken'
else:
self.small_text = self._small_text_from_node()
@property
def is_folder(self) -> bool:
return self._is_folder
@property
def is_spinning(self) -> bool:
return self._is_spinning
def update_placement(self, x, y, width, height):
"""Use OpenGL to draw this one menu item."""
self.x = x
self.y = y
self.width = width
self.height = height
def draw(self, highlighted: bool):
bgl.glEnable(bgl.GL_BLEND)
if highlighted:
color = (0.555, 0.555, 0.555, 0.8)
else:
color = (0.447, 0.447, 0.447, 0.8)
draw.aabox((self.x, self.y), (self.x + self.width, self.y + self.height), color)
texture = self.icon
if texture:
err = texture.gl_load(filter=bgl.GL_NEAREST, mag=bgl.GL_NEAREST)
assert not err, 'OpenGL error: %i' % err
# ------ TEXTURE ---------#
if texture:
draw.bind_texture(texture)
bgl.glBlendFunc(bgl.GL_SRC_ALPHA, bgl.GL_ONE_MINUS_SRC_ALPHA)
draw.aabox_with_texture(
(self.x + self.icon_margin_x, self.y),
(self.x + self.icon_margin_x + ICON_WIDTH, self.y + ICON_HEIGHT),
)
bgl.glDisable(bgl.GL_BLEND)
if texture:
texture.gl_free()
# draw some text
text_x = self.x + self.icon_margin_x + ICON_WIDTH + self.text_margin_x
text_y = self.y + ICON_HEIGHT * 0.5 - 0.25 * self.text_size
draw.text((text_x, text_y), self.label_text, fsize=self.text_size)
draw.text((text_x, self.y + 0.5 * self.text_size_small), self.small_text,
fsize=self.text_size_small, rgba=(1.0, 1.0, 1.0, 0.5))
def hits(self, mouse_x: int, mouse_y: int) -> bool:
return self.x < mouse_x < self.x + self.width and self.y < mouse_y < self.y + self.height

View File

@@ -0,0 +1,26 @@
import pillarsdk
class SpecialFolderNode(pillarsdk.Node):
NODE_TYPE = 'SPECIAL'
class UpNode(SpecialFolderNode):
NODE_TYPE = 'UP'
def __init__(self):
super().__init__()
self['_id'] = 'UP'
self['node_type'] = self.NODE_TYPE
class ProjectNode(SpecialFolderNode):
NODE_TYPE = 'PROJECT'
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'] = self.NODE_TYPE

View File

@@ -18,6 +18,7 @@
import json import json
import pathlib import pathlib
import typing
def sizeof_fmt(num: int, suffix='B') -> str: def sizeof_fmt(num: int, suffix='B') -> str:
@@ -29,12 +30,12 @@ def sizeof_fmt(num: int, suffix='B') -> str:
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
if abs(num) < 1024: if abs(num) < 1024:
return '%.1f %s%s' % (num, unit, suffix) return '%.1f %s%s' % (num, unit, suffix)
num /= 1024 num //= 1024
return '%.1f Yi%s' % (num, suffix) return '%.1f Yi%s' % (num, suffix)
def find_in_path(path: pathlib.Path, filename: str) -> pathlib.Path: def find_in_path(path: pathlib.Path, filename: str) -> typing.Optional[pathlib.Path]:
"""Performs a breadth-first search for the filename. """Performs a breadth-first search for the filename.
Returns the path that contains the file, or None if not found. Returns the path that contains the file, or None if not found.

View File

@@ -236,7 +236,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.10.0', version='1.11.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('.'),