Moved GUI to 3D viewport
We now draw the GUI using OpenGL in Python. This allows for much more control on the Python side. It's still a prototype, and allows us to test the features without depending on C support in Blender itself. GUI code was taken from the Asset Flinger addon.
This commit is contained in:
parent
55eea4a9dc
commit
59401d9c41
@ -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, 1, 0),
|
"version": (0, 2, 0),
|
||||||
"blender": (2, 77, 0),
|
"blender": (2, 77, 0),
|
||||||
"location": "TO BE DETERMINED",
|
"location": "TO BE DETERMINED",
|
||||||
"description": "Allows downloading of textures from the Blender Cloud. Requires "
|
"description": "Allows downloading of textures from the Blender Cloud. Requires "
|
||||||
@ -32,23 +32,19 @@ bl_info = {
|
|||||||
"support": "TESTING"
|
"support": "TESTING"
|
||||||
}
|
}
|
||||||
|
|
||||||
import os.path
|
|
||||||
import typing
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
# Support reloading
|
# Support reloading
|
||||||
if 'pillar' in locals():
|
if 'pillar' in locals():
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
pillar = importlib.reload(pillar)
|
pillar = importlib.reload(pillar)
|
||||||
async_loop = importlib.reload(async_loop)
|
async_loop = importlib.reload(async_loop)
|
||||||
|
gui = importlib.reload(gui)
|
||||||
else:
|
else:
|
||||||
from . import pillar, async_loop
|
from . import pillar, async_loop, gui
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import bpy.utils.previews
|
from bpy.types import AddonPreferences, Operator, WindowManager
|
||||||
from bpy.types import AddonPreferences, Operator, PropertyGroup, WindowManager
|
from bpy.props import StringProperty
|
||||||
from bpy.props import PointerProperty, StringProperty, EnumProperty
|
|
||||||
|
|
||||||
|
|
||||||
class BlenderCloudPreferences(AddonPreferences):
|
class BlenderCloudPreferences(AddonPreferences):
|
||||||
@ -132,134 +128,9 @@ class PillarCredentialsUpdate(Operator):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
# We can store multiple preview collections here,
|
|
||||||
# however in this example we only store "main"
|
|
||||||
preview_collections = {}
|
|
||||||
|
|
||||||
|
|
||||||
def enum_previews_from_directory_items(self, context) -> typing.List[typing.AnyStr]:
|
|
||||||
"""EnumProperty callback"""
|
|
||||||
|
|
||||||
if context is None:
|
|
||||||
return []
|
|
||||||
|
|
||||||
wm = context.window_manager
|
|
||||||
project_uuid = wm.blender_cloud_project
|
|
||||||
node_uuid = wm.blender_cloud_node
|
|
||||||
|
|
||||||
# Get the preview collection (defined in register func).
|
|
||||||
pcoll = preview_collections["blender_cloud"]
|
|
||||||
if pcoll.project_uuid == project_uuid and pcoll.node_uuid == node_uuid:
|
|
||||||
return pcoll.previews
|
|
||||||
|
|
||||||
print('Loading previews for project {!r} node {!r}'.format(project_uuid, node_uuid))
|
|
||||||
|
|
||||||
if pcoll.async_task is not None and not pcoll.async_task.done():
|
|
||||||
# We're still asynchronously downloading, but the UUIDs changed.
|
|
||||||
print('Cancelling running async download task {}'.format(pcoll.async_task))
|
|
||||||
pcoll.async_task.cancel()
|
|
||||||
|
|
||||||
# Download the previews asynchronously.
|
|
||||||
pcoll.previews = []
|
|
||||||
pcoll.project_uuid = project_uuid
|
|
||||||
pcoll.node_uuid = node_uuid
|
|
||||||
pcoll.async_task = asyncio.ensure_future(async_download_previews(wm.thumbnails_cache, pcoll))
|
|
||||||
|
|
||||||
# Start the async manager so everything happens.
|
|
||||||
async_loop.ensure_async_loop()
|
|
||||||
|
|
||||||
return pcoll.previews
|
|
||||||
|
|
||||||
|
|
||||||
async def async_download_previews(thumbnails_directory, pcoll):
|
|
||||||
# If we have a node UUID, we fetch the textures
|
|
||||||
# FIXME: support mixture of sub-nodes and textures under one node.
|
|
||||||
enum_items = pcoll.previews
|
|
||||||
|
|
||||||
node_uuid = pcoll.node_uuid
|
|
||||||
project_uuid = pcoll.project_uuid
|
|
||||||
|
|
||||||
def thumbnail_loading(file_desc):
|
|
||||||
# TODO: trigger re-draw
|
|
||||||
pass
|
|
||||||
|
|
||||||
def thumbnail_loaded(file_desc, thumb_path):
|
|
||||||
thumb = pcoll.get(thumb_path)
|
|
||||||
if thumb is None:
|
|
||||||
thumb = pcoll.load(thumb_path, thumb_path, 'IMAGE')
|
|
||||||
enum_items.append(('thumb-{}'.format(thumb_path), file_desc['filename'],
|
|
||||||
thumb_path,
|
|
||||||
thumb.icon_id,
|
|
||||||
len(enum_items)))
|
|
||||||
# TODO: trigger re-draw
|
|
||||||
|
|
||||||
if node_uuid:
|
|
||||||
# Make sure we can go up again.
|
|
||||||
parent = await pillar.parent_node_uuid(node_uuid)
|
|
||||||
enum_items.append(('node-{}'.format(parent), 'up', 'up',
|
|
||||||
'FILE_FOLDER',
|
|
||||||
len(enum_items)))
|
|
||||||
|
|
||||||
directory = os.path.join(thumbnails_directory, project_uuid, node_uuid)
|
|
||||||
os.makedirs(directory, exist_ok=True)
|
|
||||||
|
|
||||||
await pillar.fetch_texture_thumbs(node_uuid, 's', directory,
|
|
||||||
thumbnail_loading=thumbnail_loading,
|
|
||||||
thumbnail_loaded=thumbnail_loaded)
|
|
||||||
elif project_uuid:
|
|
||||||
children = await pillar.get_nodes(project_uuid, '')
|
|
||||||
|
|
||||||
for child in children:
|
|
||||||
print(' - %(_id)s = %(name)s' % child)
|
|
||||||
enum_items.append(('node-{}'.format(child['_id']), child['name'],
|
|
||||||
'description',
|
|
||||||
'FILE_FOLDER',
|
|
||||||
len(enum_items)))
|
|
||||||
# TODO: trigger re-draw
|
|
||||||
else:
|
|
||||||
# TODO: add "nothing here" icon and trigger re-draw
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def enum_previews_from_directory_update(self, context):
|
|
||||||
print('Updating from {!r}'.format(self.blender_cloud_thumbnails))
|
|
||||||
|
|
||||||
sel_type, sel_id = self.blender_cloud_thumbnails.split('-', 1)
|
|
||||||
|
|
||||||
if sel_type == 'node':
|
|
||||||
# Go into this node
|
|
||||||
self.blender_cloud_node = sel_id
|
|
||||||
elif sel_type == 'thumb':
|
|
||||||
# Select this image
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
print("enum_previews_from_directory_update: Don't know what to do with {!r}"
|
|
||||||
.format(self.blender_cloud_thumbnails))
|
|
||||||
|
|
||||||
|
|
||||||
class PreviewsExamplePanel(bpy.types.Panel):
|
|
||||||
"""Creates a Panel in the Object properties window"""
|
|
||||||
|
|
||||||
bl_label = "Previews Example Panel"
|
|
||||||
bl_idname = "OBJECT_PT_previews"
|
|
||||||
bl_space_type = 'PROPERTIES'
|
|
||||||
bl_region_type = 'WINDOW'
|
|
||||||
bl_context = "object"
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
wm = context.window_manager
|
|
||||||
|
|
||||||
row = layout.column()
|
|
||||||
row.prop(wm, "thumbnails_cache")
|
|
||||||
row.prop(wm, "blender_cloud_project")
|
|
||||||
row.prop(wm, "blender_cloud_node")
|
|
||||||
row.template_icon_view(wm, "blender_cloud_thumbnails", show_labels=True)
|
|
||||||
# row.prop(wm, "blender_cloud_thumbnails")
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
bpy.utils.register_module(__name__)
|
bpy.utils.register_class(BlenderCloudPreferences)
|
||||||
|
bpy.utils.register_class(PillarCredentialsUpdate)
|
||||||
|
|
||||||
WindowManager.thumbnails_cache = StringProperty(
|
WindowManager.thumbnails_cache = StringProperty(
|
||||||
name="Thumbnails cache",
|
name="Thumbnails cache",
|
||||||
@ -274,40 +145,19 @@ def register():
|
|||||||
name="Blender Cloud node UUID",
|
name="Blender Cloud node UUID",
|
||||||
default='') # empty == top-level of project
|
default='') # empty == top-level of project
|
||||||
|
|
||||||
WindowManager.blender_cloud_thumbnails = EnumProperty(
|
gui.register()
|
||||||
items=enum_previews_from_directory_items,
|
|
||||||
update=enum_previews_from_directory_update,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Note that preview collections returned by bpy.utils.previews
|
|
||||||
# are regular Python objects - you can use them to store custom data.
|
|
||||||
#
|
|
||||||
# This is especially useful here, since:
|
|
||||||
# - It avoids us regenerating the whole enum over and over.
|
|
||||||
# - It can store enum_items' strings
|
|
||||||
# (remember you have to keep those strings somewhere in py,
|
|
||||||
# else they get freed and Blender references invalid memory!).
|
|
||||||
pcoll = bpy.utils.previews.new()
|
|
||||||
pcoll.previews = ()
|
|
||||||
pcoll.project_uuid = ''
|
|
||||||
pcoll.node_uuid = ''
|
|
||||||
pcoll.async_task = None
|
|
||||||
|
|
||||||
preview_collections["blender_cloud"] = pcoll
|
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
bpy.utils.unregister_module(__name__)
|
gui.unregister()
|
||||||
|
|
||||||
|
bpy.utils.unregister_class(PillarCredentialsUpdate)
|
||||||
|
bpy.utils.unregister_class(BlenderCloudPreferences)
|
||||||
|
|
||||||
del WindowManager.thumbnails_cache
|
|
||||||
del WindowManager.blender_cloud_project
|
del WindowManager.blender_cloud_project
|
||||||
del WindowManager.blender_cloud_node
|
del WindowManager.blender_cloud_node
|
||||||
del WindowManager.blender_cloud_thumbnails
|
del WindowManager.blender_cloud_thumbnails
|
||||||
|
|
||||||
for pcoll in preview_collections.values():
|
|
||||||
bpy.utils.previews.remove(pcoll)
|
|
||||||
preview_collections.clear()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
register()
|
register()
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
"""Manages the asyncio loop."""
|
"""Manages the asyncio loop."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import traceback
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
@ -12,11 +14,27 @@ def kick_async_loop(*args):
|
|||||||
stop_async_loop()
|
stop_async_loop()
|
||||||
return
|
return
|
||||||
|
|
||||||
if not asyncio.Task.all_tasks():
|
all_tasks = asyncio.Task.all_tasks()
|
||||||
|
if not all_tasks:
|
||||||
print('{}: no more scheduled tasks, stopping'.format(__name__))
|
print('{}: no more scheduled tasks, stopping'.format(__name__))
|
||||||
stop_async_loop()
|
stop_async_loop()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if all(task.done() for task in all_tasks):
|
||||||
|
print('{}: all tasks are done, fetching results and stopping.'.format(__name__))
|
||||||
|
for task in all_tasks:
|
||||||
|
# noinspection PyBroadException
|
||||||
|
try:
|
||||||
|
task.result()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
# No problem, we want to stop anyway.
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
print('{}: resulted in exception'.format(task))
|
||||||
|
traceback.print_exc()
|
||||||
|
stop_async_loop()
|
||||||
|
return
|
||||||
|
|
||||||
# Perform a single async loop step
|
# Perform a single async loop step
|
||||||
async def do_nothing():
|
async def do_nothing():
|
||||||
pass
|
pass
|
||||||
@ -43,7 +61,3 @@ def stop_async_loop():
|
|||||||
if handler is None:
|
if handler is None:
|
||||||
return
|
return
|
||||||
bpy.app.handlers.scene_update_pre.remove(handler)
|
bpy.app.handlers.scene_update_pre.remove(handler)
|
||||||
|
|
||||||
# Optional: cancel all pending tasks.
|
|
||||||
# for task in asyncio.Task.all_tasks():
|
|
||||||
# task.cancel()
|
|
||||||
|
414
blender_cloud/gui.py
Normal file
414
blender_cloud/gui.py
Normal file
@ -0,0 +1,414 @@
|
|||||||
|
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||||
|
#
|
||||||
|
# Copyright (C) 2014 Blender Aid
|
||||||
|
# http://www.blendearaid.com
|
||||||
|
# blenderaid@gmail.com
|
||||||
|
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# ##### END GPL LICENSE BLOCK #####
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import bgl
|
||||||
|
import blf
|
||||||
|
import os
|
||||||
|
|
||||||
|
from bpy.types import AddonPreferences
|
||||||
|
from bpy.props import (BoolProperty, EnumProperty,
|
||||||
|
FloatProperty, FloatVectorProperty,
|
||||||
|
IntProperty, StringProperty)
|
||||||
|
|
||||||
|
from . import async_loop, pillar
|
||||||
|
|
||||||
|
icon_width = 128
|
||||||
|
icon_height = 128
|
||||||
|
target_item_width = 400
|
||||||
|
target_item_height = 128
|
||||||
|
|
||||||
|
library_path = '/tmp'
|
||||||
|
library_icons_path = os.path.join(os.path.dirname(__file__), "icons")
|
||||||
|
|
||||||
|
|
||||||
|
class MenuItem:
|
||||||
|
"""GUI menu item for the 3D View GUI."""
|
||||||
|
|
||||||
|
icon_margin_x = 4
|
||||||
|
icon_margin_y = 4
|
||||||
|
text_margin_x = 6
|
||||||
|
|
||||||
|
text_height = 16
|
||||||
|
text_width = 72
|
||||||
|
|
||||||
|
DEFAULT_ICONS = {
|
||||||
|
'FOLDER': os.path.join(library_icons_path, 'folder.png'),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, node_uuid: str, file_desc, thumb_path: str, label_text):
|
||||||
|
self.node_uuid = node_uuid # pillarsdk.Node UUID
|
||||||
|
self.file_desc = file_desc # pillarsdk.File object, or None if a 'folder' node.
|
||||||
|
self.label_text = label_text
|
||||||
|
|
||||||
|
thumb_path = self.DEFAULT_ICONS.get(thumb_path, thumb_path)
|
||||||
|
self.thumb_path = thumb_path
|
||||||
|
self.icon = bpy.data.images.load(filepath=thumb_path)
|
||||||
|
|
||||||
|
# Updated when drawing the image
|
||||||
|
self.x = 0
|
||||||
|
self.y = 0
|
||||||
|
self.width = 0
|
||||||
|
self.height = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_folder(self) -> bool:
|
||||||
|
return self.file_desc is None
|
||||||
|
|
||||||
|
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
|
||||||
|
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 ---------#
|
||||||
|
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)
|
||||||
|
|
||||||
|
texture.gl_free()
|
||||||
|
|
||||||
|
# draw some text
|
||||||
|
font_id = 0
|
||||||
|
blf.position(font_id,
|
||||||
|
self.x + self.icon_margin_x + icon_width + self.text_margin_x,
|
||||||
|
self.y + icon_height * 0.5 - 0.25 * self.text_height, 0)
|
||||||
|
blf.size(font_id, self.text_height, self.text_width)
|
||||||
|
blf.draw(font_id, self.label_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(bpy.types.Operator):
|
||||||
|
bl_idname = 'pillar.browser'
|
||||||
|
bl_label = 'Blender Cloud Texture Browser'
|
||||||
|
|
||||||
|
_draw_handle = None
|
||||||
|
|
||||||
|
project_uuid = '5672beecc0261b2005ed1a33' # Blender Cloud project UUID
|
||||||
|
node_uuid = '' # Blender Cloud node UUID
|
||||||
|
async_task = None # asyncio task for fetching thumbnails
|
||||||
|
timer = None
|
||||||
|
|
||||||
|
current_path = ''
|
||||||
|
current_display_content = []
|
||||||
|
loaded_images = set()
|
||||||
|
thumbnails_cache = ''
|
||||||
|
|
||||||
|
mouse_x = 0
|
||||||
|
mouse_y = 0
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
if context.area.type != 'VIEW_3D':
|
||||||
|
self.report({'WARNING'}, "View3D not found, cannot show asset flinger")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
print('Area is %s' % context.area)
|
||||||
|
|
||||||
|
wm = context.window_manager
|
||||||
|
self.thumbnails_cache = wm.thumbnails_cache
|
||||||
|
self.project_uuid = wm.blender_cloud_project
|
||||||
|
self.node_uuid = wm.blender_cloud_node
|
||||||
|
|
||||||
|
self.mouse_x = event.mouse_region_x
|
||||||
|
self.mouse_y = event.mouse_region_y
|
||||||
|
|
||||||
|
# Add the region OpenGL drawing callback
|
||||||
|
# draw in view space with 'POST_VIEW' and 'PRE_VIEW'
|
||||||
|
self._draw_handle = bpy.types.SpaceView3D.draw_handler_add(
|
||||||
|
self.draw_menu, (context,), 'WINDOW', 'POST_PIXEL')
|
||||||
|
|
||||||
|
self.current_display_content = []
|
||||||
|
self.loaded_images = set()
|
||||||
|
self.browse_assets(context)
|
||||||
|
|
||||||
|
context.window_manager.modal_handler_add(self)
|
||||||
|
self.timer = context.window_manager.event_timer_add(1/30, context.window)
|
||||||
|
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def modal(self, context, event):
|
||||||
|
if event.type == 'TIMER':
|
||||||
|
context.area.tag_redraw()
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
if 'MOUSE' in event.type:
|
||||||
|
context.area.tag_redraw()
|
||||||
|
self.mouse_x = event.mouse_region_x
|
||||||
|
self.mouse_y = event.mouse_region_y
|
||||||
|
|
||||||
|
if event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
|
||||||
|
selected = self.get_clicked()
|
||||||
|
|
||||||
|
if selected is None:
|
||||||
|
self._finish(context)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
if selected.is_folder:
|
||||||
|
self.node_uuid = selected.node_uuid
|
||||||
|
self.browse_assets(context)
|
||||||
|
else:
|
||||||
|
self.handle_item_selection(selected)
|
||||||
|
self._finish(context)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
elif event.type in {'RIGHTMOUSE', 'ESC'}:
|
||||||
|
self._finish(context)
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def _stop_async_task(self):
|
||||||
|
if self.async_task is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.async_task.done():
|
||||||
|
print('Cancelling running async download task {}'.format(self.async_task))
|
||||||
|
self.async_task.cancel()
|
||||||
|
else:
|
||||||
|
self.async_task.result() # This re-raises any exception of the task.
|
||||||
|
|
||||||
|
def _finish(self, context):
|
||||||
|
self._stop_async_task()
|
||||||
|
bpy.types.SpaceView3D.draw_handler_remove(self._draw_handle, 'WINDOW')
|
||||||
|
context.window_manager.event_timer_remove(self.timer)
|
||||||
|
context.area.tag_redraw()
|
||||||
|
|
||||||
|
def clear_images(self):
|
||||||
|
"""Removes all images we loaded from Blender's memory."""
|
||||||
|
|
||||||
|
for image in bpy.data.images:
|
||||||
|
if image.filepath_raw not in self.loaded_images:
|
||||||
|
continue
|
||||||
|
|
||||||
|
image.user_clear()
|
||||||
|
bpy.data.images.remove(image)
|
||||||
|
|
||||||
|
self.loaded_images.clear()
|
||||||
|
self.current_display_content.clear()
|
||||||
|
|
||||||
|
def add_menu_item(self, *args, menu_item_lock=threading.Lock()) -> MenuItem:
|
||||||
|
menu_item = MenuItem(*args)
|
||||||
|
|
||||||
|
# Just make this thread-safe to be on the safe side.
|
||||||
|
with menu_item_lock:
|
||||||
|
self.current_display_content.append(menu_item)
|
||||||
|
self.loaded_images.add(menu_item.icon.filepath_raw)
|
||||||
|
|
||||||
|
return menu_item
|
||||||
|
|
||||||
|
async def async_download_previews(self, context, thumbnails_directory):
|
||||||
|
# If we have a node UUID, we fetch the textures
|
||||||
|
# FIXME: support mixture of sub-nodes and textures under one node.
|
||||||
|
self.clear_images()
|
||||||
|
|
||||||
|
def redraw():
|
||||||
|
# region = context.region
|
||||||
|
# if region is None:
|
||||||
|
# print('Unable to redraw, region is %s' % region)
|
||||||
|
# print(' (context is %s)' % context)
|
||||||
|
# return
|
||||||
|
# region.tag_redraw()
|
||||||
|
pass
|
||||||
|
|
||||||
|
def thumbnail_loading(node_uuid, file_desc):
|
||||||
|
# TODO: add MenuItem
|
||||||
|
redraw()
|
||||||
|
|
||||||
|
def thumbnail_loaded(node_uuid, file_desc, thumb_path):
|
||||||
|
# Add MenuItem; TODO: update MenuItem added above
|
||||||
|
self.add_menu_item(node_uuid, file_desc, thumb_path, file_desc['filename'])
|
||||||
|
redraw()
|
||||||
|
|
||||||
|
if self.node_uuid:
|
||||||
|
# Make sure we can go up again.
|
||||||
|
parent_uuid = await pillar.parent_node_uuid(self.node_uuid)
|
||||||
|
self.add_menu_item(parent_uuid, None, 'FOLDER', '.. up ..')
|
||||||
|
|
||||||
|
directory = os.path.join(thumbnails_directory, self.project_uuid, self.node_uuid)
|
||||||
|
os.makedirs(directory, exist_ok=True)
|
||||||
|
|
||||||
|
await pillar.fetch_texture_thumbs(self.node_uuid, 's', directory,
|
||||||
|
thumbnail_loading=thumbnail_loading,
|
||||||
|
thumbnail_loaded=thumbnail_loaded)
|
||||||
|
elif self.project_uuid:
|
||||||
|
children = await pillar.get_nodes(self.project_uuid, '')
|
||||||
|
|
||||||
|
for child in children:
|
||||||
|
print(' - %(_id)s = %(name)s' % child)
|
||||||
|
self.add_menu_item(child['_id'], None, 'FOLDER', child['name'])
|
||||||
|
redraw()
|
||||||
|
else:
|
||||||
|
# TODO: add "nothing here" icon and trigger re-draw
|
||||||
|
redraw()
|
||||||
|
|
||||||
|
# Call the 'done' callback.
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.call_soon_threadsafe(self.downloading_done)
|
||||||
|
|
||||||
|
def browse_assets(self, context):
|
||||||
|
self._stop_async_task()
|
||||||
|
self.clear_images()
|
||||||
|
|
||||||
|
# Download the previews asynchronously.
|
||||||
|
self.async_task = asyncio.ensure_future(
|
||||||
|
self.async_download_previews(context, self.thumbnails_cache))
|
||||||
|
|
||||||
|
# Start the async manager so everything happens.
|
||||||
|
async_loop.ensure_async_loop()
|
||||||
|
|
||||||
|
def downloading_done(self):
|
||||||
|
# if not self.async_task.done():
|
||||||
|
# print('%s: aborting download task' % self)
|
||||||
|
# self._stop_async_task()
|
||||||
|
# else:
|
||||||
|
# print('%s: downloading done' % self)
|
||||||
|
# self.async_task.result()
|
||||||
|
pass
|
||||||
|
|
||||||
|
def draw_menu(self, context):
|
||||||
|
margin_x = 20
|
||||||
|
margin_y = 5
|
||||||
|
padding_x = 5
|
||||||
|
|
||||||
|
content_width = context.area.regions[4].width - margin_x * 2
|
||||||
|
content_height = context.area.regions[4].height - margin_y * 2
|
||||||
|
|
||||||
|
content_x = margin_x
|
||||||
|
content_y = context.area.height - margin_y - target_item_height - 50
|
||||||
|
|
||||||
|
col_count = content_width // target_item_width
|
||||||
|
|
||||||
|
item_width = (content_width - (col_count * padding_x)) / col_count
|
||||||
|
item_height = target_item_height
|
||||||
|
|
||||||
|
block_width = item_width + padding_x
|
||||||
|
block_height = item_height + margin_y
|
||||||
|
|
||||||
|
bgl.glEnable(bgl.GL_BLEND)
|
||||||
|
bgl.glColor4f(0.0, 0.0, 0.0, 0.6)
|
||||||
|
bgl.glRectf(0, 0, context.area.regions[4].width, context.area.regions[4].height)
|
||||||
|
|
||||||
|
if self.current_display_content:
|
||||||
|
for item_idx, item in enumerate(self.current_display_content):
|
||||||
|
x = (item_idx % col_count) * block_width
|
||||||
|
y = content_y - (item_idx // col_count) * block_height
|
||||||
|
|
||||||
|
item.update_placement(x, y, item_width, item_height)
|
||||||
|
item.draw(highlighted=item.hits(self.mouse_x, self.mouse_y))
|
||||||
|
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.glColor4f(0.0, 0.0, 0.0, 1.0)
|
||||||
|
|
||||||
|
def get_clicked(self) -> MenuItem:
|
||||||
|
|
||||||
|
for item in self.current_display_content:
|
||||||
|
if item.hits(self.mouse_x, self.mouse_y):
|
||||||
|
return item
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def handle_item_selection(self, item: MenuItem):
|
||||||
|
"""Called when the user clicks on a menu item that doesn't represent a folder."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# store keymaps here to access after registration
|
||||||
|
addon_keymaps = []
|
||||||
|
|
||||||
|
|
||||||
|
def menu_draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.separator()
|
||||||
|
layout.operator(BlenderCloudBrowser.bl_idname, icon='MOD_SCREW')
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
bpy.utils.register_class(BlenderCloudBrowser)
|
||||||
|
bpy.types.INFO_MT_mesh_add.append(menu_draw)
|
||||||
|
|
||||||
|
# handle the keymap
|
||||||
|
wm = bpy.context.window_manager
|
||||||
|
kc = wm.keyconfigs.addon
|
||||||
|
if not kc:
|
||||||
|
print('No addon key configuration space found, so no custom hotkeys added.')
|
||||||
|
return
|
||||||
|
|
||||||
|
km = kc.keymaps.new(name='3D View', space_type='VIEW_3D')
|
||||||
|
kmi = km.keymap_items.new('pillar.browser', 'A', 'PRESS', ctrl=True, shift=True, alt=True)
|
||||||
|
addon_keymaps.append((km, kmi))
|
||||||
|
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
bpy.utils.unregister_class(BlenderCloudBrowser)
|
||||||
|
|
||||||
|
# handle the keymap
|
||||||
|
for km, kmi in addon_keymaps:
|
||||||
|
km.keymap_items.remove(kmi)
|
||||||
|
addon_keymaps.clear()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
register()
|
BIN
blender_cloud/icons/folder.png
Normal file
BIN
blender_cloud/icons/folder.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
@ -165,11 +165,11 @@ async def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str,
|
|||||||
@param parent_node_uuid: the UUID of the parent node. All sub-nodes will be downloaded.
|
@param parent_node_uuid: the UUID of the parent node. All sub-nodes will be downloaded.
|
||||||
@param desired_size: size indicator, from 'sbtmlh'.
|
@param desired_size: size indicator, from 'sbtmlh'.
|
||||||
@param thumbnail_directory: directory in which to store the downloaded thumbnails.
|
@param thumbnail_directory: directory in which to store the downloaded thumbnails.
|
||||||
@param thumbnail_loading: callback function that takes (pillarsdk.File object)
|
@param thumbnail_loading: callback function that takes (node_id, pillarsdk.File object)
|
||||||
parameter, which is called before a thumbnail will be downloaded. This allows you to
|
parameters, which is called before a thumbnail will be downloaded. This allows you to
|
||||||
show a "downloading" indicator.
|
show a "downloading" indicator.
|
||||||
@param thumbnail_loaded: callback function that takes (pillarsdk.File object, thumbnail path)
|
@param thumbnail_loaded: callback function that takes (node_id, pillarsdk.File object,
|
||||||
parameters, which is called for every thumbnail after it's been downloaded.
|
thumbnail path) parameters, which is called for every thumbnail after it's been downloaded.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
api = pillar_api()
|
api = pillar_api()
|
||||||
@ -187,7 +187,9 @@ async def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str,
|
|||||||
# Find the File that belongs to this texture node
|
# Find the File that belongs to this texture node
|
||||||
pic_uuid = texture_node['picture']
|
pic_uuid = texture_node['picture']
|
||||||
file_desc = await loop.run_in_executor(None, file_find, pic_uuid)
|
file_desc = await loop.run_in_executor(None, file_find, pic_uuid)
|
||||||
loop.call_soon_threadsafe(functools.partial(thumbnail_loading, file_desc))
|
loop.call_soon_threadsafe(functools.partial(thumbnail_loading,
|
||||||
|
texture_node['_id'],
|
||||||
|
file_desc))
|
||||||
|
|
||||||
if file_desc is None:
|
if file_desc is None:
|
||||||
print('Unable to find file for texture node {}'.format(pic_uuid))
|
print('Unable to find file for texture node {}'.format(pic_uuid))
|
||||||
@ -197,7 +199,9 @@ async def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str,
|
|||||||
thumb_path = await stream_thumb_to_file(file_desc, thumbnail_directory, desired_size)
|
thumb_path = await stream_thumb_to_file(file_desc, thumbnail_directory, desired_size)
|
||||||
# print('Texture node {} has file {}'.format(texture_node['_id'], thumb_path))
|
# print('Texture node {} has file {}'.format(texture_node['_id'], thumb_path))
|
||||||
|
|
||||||
loop.call_soon_threadsafe(functools.partial(thumbnail_loaded, file_desc, thumb_path))
|
loop.call_soon_threadsafe(functools.partial(thumbnail_loaded,
|
||||||
|
texture_node['_id'],
|
||||||
|
file_desc, thumb_path))
|
||||||
|
|
||||||
# Download all texture nodes in parallel.
|
# Download all texture nodes in parallel.
|
||||||
texture_nodes = await get_nodes(parent_node_uuid=parent_node_uuid)
|
texture_nodes = await get_nodes(parent_node_uuid=parent_node_uuid)
|
||||||
|
Reference in New Issue
Block a user