WIP: asset engine support
This commit is contained in:
parent
ba7734aaa5
commit
fc514b2d73
@ -73,14 +73,16 @@ def register():
|
||||
blender = reload_mod('blender')
|
||||
gui = reload_mod('gui')
|
||||
async_loop = reload_mod('async_loop')
|
||||
asset_engine = reload_mod('asset_engine')
|
||||
else:
|
||||
from . import blender, gui, async_loop
|
||||
from . import blender, gui, async_loop, asset_engine
|
||||
|
||||
async_loop.setup_asyncio_executor()
|
||||
async_loop.register()
|
||||
|
||||
blender.register()
|
||||
gui.register()
|
||||
asset_engine.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
|
468
blender_cloud/asset_engine.py
Normal file
468
blender_cloud/asset_engine.py
Normal file
@ -0,0 +1,468 @@
|
||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
||||
#
|
||||
# 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 2
|
||||
# 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, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ##### END GPL LICENSE BLOCK #####
|
||||
|
||||
# <pep8 compliant>
|
||||
|
||||
"""Blender Cloud interface for the Asset Engine."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import bpy
|
||||
import time
|
||||
from bpy.types import (AssetEngine, AssetList, FileSelectParams,
|
||||
AssetUUIDList, AssetUUID,
|
||||
Panel, PropertyGroup, UIList)
|
||||
from bpy.props import (StringProperty,
|
||||
BoolProperty,
|
||||
IntProperty,
|
||||
FloatProperty,
|
||||
EnumProperty,
|
||||
CollectionProperty)
|
||||
|
||||
from . import async_loop, pillar, cache
|
||||
|
||||
ASSET_ENGINE_ID = 0xc0ffee
|
||||
|
||||
|
||||
def object_id_to_ea_uuid(pillar_object_id: str) -> tuple:
|
||||
"""Turns a ObjectId string from Pillar to a tuple of 4 ints.
|
||||
|
||||
>>> object_id_to_ea_uuid('55f2d0dc2beb33006e43dd7e')
|
||||
(12648430, 1441976540, 736834304, 1849941374)
|
||||
>>> object_id_to_ea_uuid('55f2d0dc2beb33006e43dd7e') == \
|
||||
(ASSET_ENGINE_ID, 0x55f2d0dc, 0x2beb3300, 0x6e43dd7e)
|
||||
True
|
||||
|
||||
The first int is hard-coded to indicate this asset engine.
|
||||
The other three ints are 32 bit each, and are taken from the 12-byte
|
||||
ObjectId (see https://docs.mongodb.org/manual/reference/method/ObjectId/)
|
||||
"""
|
||||
|
||||
# Make sure it's a 12-byte number in hex.
|
||||
pillar_object_id = pillar_object_id.rjust(24, '0')
|
||||
|
||||
return (ASSET_ENGINE_ID,
|
||||
int(pillar_object_id[0:8], 16),
|
||||
int(pillar_object_id[8:16], 16),
|
||||
int(pillar_object_id[16:24], 16))
|
||||
|
||||
|
||||
class BCloudAssetEngineDirListJob:
|
||||
def __init__(self, job_id: int, path: pillar.CloudPath, future: asyncio.Future = None):
|
||||
self.log = logging.getLogger('%s.%s' % (__name__, BCloudAssetEngineDirListJob.__qualname__))
|
||||
self.log.debug('Starting new dirlist job (id=%i) for path %r', job_id, path)
|
||||
|
||||
self.job_id = job_id
|
||||
self.status = {'INVALID'}
|
||||
self.progress = 0.0
|
||||
self.path = path
|
||||
|
||||
# Start a new asynchronous task.
|
||||
self.signalling_future = future or asyncio.Future()
|
||||
# self.async_task = asyncio.ensure_future(self.async_download_previews())
|
||||
# self.log.debug('Created new task %r', self.async_task)
|
||||
# self.status = {'VALID', 'RUNNING'}
|
||||
|
||||
self.async_task = None
|
||||
self.status = {'VALID'}
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(job_id=%i, path=%s, future=%s)' % (type(self), self.job_id, self.path,
|
||||
self.signalling_future)
|
||||
|
||||
def stop(self):
|
||||
self.log.debug('Stopping async task')
|
||||
if self.async_task is None:
|
||||
self.log.debug('No async task, trivially stopped')
|
||||
return
|
||||
|
||||
# Signal that we want to stop.
|
||||
if not self.signalling_future.done():
|
||||
self.log.info("Signalling that we want to cancel anything that's running.")
|
||||
self.signalling_future.cancel()
|
||||
|
||||
# Wait until the asynchronous task is done.
|
||||
if not self.async_task.done():
|
||||
# TODO: Should we really block? Or let it disappear into the background?
|
||||
self.log.info("blocking until async task is done.")
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
loop.run_until_complete(self.async_task)
|
||||
except asyncio.CancelledError:
|
||||
self.log.info('Asynchronous task was cancelled')
|
||||
return
|
||||
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
self.async_task.result() # This re-raises any exception of the task.
|
||||
except asyncio.CancelledError:
|
||||
self.log.info('Asynchronous task was cancelled')
|
||||
except Exception:
|
||||
self.log.exception("Exception from asynchronous task")
|
||||
|
||||
async def async_download_previews(self):
|
||||
self.log.info('Asynchronously downloading previews')
|
||||
|
||||
def thumbnail_loading(texture_node):
|
||||
self.log.debug('Thumbnail for node %r loading', texture_node)
|
||||
# self.add_menu_item(node, None, 'SPINNER', texture_node['name'])
|
||||
|
||||
def thumbnail_loaded(texture_node, file_desc, thumb_path):
|
||||
self.log.debug('Thumbnail for node %r loaded, thumb at %r', texture_node, thumb_path)
|
||||
# self.update_menu_item(node, file_desc, thumb_path, file_desc['filename'])
|
||||
|
||||
node_uuid = self.path.node_uuid
|
||||
project_uuid = self.path.project_uuid
|
||||
|
||||
# Download either by group_texture node or project UUID (showing all top-level nodes)
|
||||
if node_uuid:
|
||||
self.log.debug('Getting subnodes for parent node %r', node_uuid)
|
||||
children = await pillar.get_nodes(parent_node_uuid=node_uuid,
|
||||
node_type='group_textures')
|
||||
elif project_uuid:
|
||||
self.log.debug('Getting subnodes for project node %r', project_uuid)
|
||||
children = await pillar.get_nodes(project_uuid, '')
|
||||
|
||||
else:
|
||||
# TODO: add "nothing here" icon and trigger re-draw
|
||||
self.log.warning("Not node UUID and no project UUID, I can't do anything!")
|
||||
return
|
||||
|
||||
# Download all child nodes
|
||||
self.log.debug('Iterating over child nodes of %r', node_uuid)
|
||||
for child in children:
|
||||
self.log.debug(' - %(_id)s = %(name)s' % child)
|
||||
# self.add_menu_item(child, None, 'FOLDER', child['name'])
|
||||
|
||||
# There are only sub-nodes at the project level, no texture nodes,
|
||||
# so we won't have to bother looking for textures.
|
||||
if not node_uuid:
|
||||
return
|
||||
|
||||
directory = cache.cache_directory('thumbnails', project_uuid, node_uuid)
|
||||
|
||||
self.log.debug('Fetching texture thumbnails for node %r to %r', node_uuid, directory)
|
||||
await pillar.fetch_texture_thumbs(node_uuid, 's', directory,
|
||||
thumbnail_loading=thumbnail_loading,
|
||||
thumbnail_loaded=thumbnail_loaded,
|
||||
future=self.signalling_future)
|
||||
|
||||
def update(self):
|
||||
self.log.debug('update()')
|
||||
async_loop.kick_async_loop()
|
||||
|
||||
if not self.async_task:
|
||||
return
|
||||
|
||||
if self.async_task.done():
|
||||
self.status = {'VALID'}
|
||||
|
||||
self.status = {'VALID', 'RUNNING'}
|
||||
|
||||
|
||||
class BCloudAssetEngine(AssetEngine):
|
||||
bl_label = "Blender Cloud"
|
||||
bl_version = 1
|
||||
|
||||
def __init__(self):
|
||||
self.log = logging.getLogger('%s.%s' % (__name__, BCloudAssetEngine.__qualname__))
|
||||
self.log.debug('Starting %s asset engine', self.bl_label)
|
||||
|
||||
self.jobs = {}
|
||||
self._next_job_id = 1
|
||||
self.path = pillar.CloudPath('/5672beecc0261b2005ed1a33')
|
||||
self.dirs = []
|
||||
self.sortedfiltered = []
|
||||
|
||||
def reset(self):
|
||||
pass
|
||||
|
||||
def _start_dirlist_job(self, path: pillar.CloudPath, job_id: int = None) -> int:
|
||||
if not job_id:
|
||||
job_id = self._next_job_id
|
||||
self._next_job_id += 1
|
||||
|
||||
self.jobs[job_id] = BCloudAssetEngineDirListJob(job_id, path)
|
||||
self.path = path
|
||||
|
||||
return job_id
|
||||
|
||||
########## PY-API only ##########
|
||||
# UI header
|
||||
def draw_header(self, layout, context):
|
||||
params = context.space_data.params
|
||||
assert isinstance(params, FileSelectParams)
|
||||
# self.log.debug('draw_header: params=%r', params)
|
||||
|
||||
# can be None when save/reload with a file selector open
|
||||
if params is None:
|
||||
return
|
||||
|
||||
is_lib_browser = params.use_library_browsing
|
||||
|
||||
layout.prop(params, "display_type", expand=True, text="")
|
||||
layout.prop(params, "sort_method", expand=True, text="")
|
||||
|
||||
layout.prop(params, "show_hidden", text="", icon='FILE_HIDDEN')
|
||||
layout.prop(params, "use_filter", text="", icon='FILTER')
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.active = params.use_filter
|
||||
|
||||
if params.filter_glob:
|
||||
# if st.active_operator and hasattr(st.active_operator, "filter_glob"):
|
||||
# row.prop(params, "filter_glob", text="")
|
||||
row.label(params.filter_glob)
|
||||
else:
|
||||
row.prop(params, "use_filter_blender", text="")
|
||||
row.prop(params, "use_filter_backup", text="")
|
||||
row.prop(params, "use_filter_image", text="")
|
||||
row.prop(params, "use_filter_movie", text="")
|
||||
row.prop(params, "use_filter_script", text="")
|
||||
row.prop(params, "use_filter_font", text="")
|
||||
row.prop(params, "use_filter_sound", text="")
|
||||
row.prop(params, "use_filter_text", text="")
|
||||
|
||||
if is_lib_browser:
|
||||
row.prop(params, "use_filter_blendid", text="")
|
||||
if params.use_filter_blendid:
|
||||
row.separator()
|
||||
row.prop(params, "filter_id_category", text="")
|
||||
|
||||
row.separator()
|
||||
row.prop(params, "filter_search", text="", icon='VIEWZOOM')
|
||||
|
||||
########## C (RNA) API ##########
|
||||
def status(self, job_id: int) -> set:
|
||||
"""Returns either {'VALID'}, {'RUNNING'} or empty set."""
|
||||
|
||||
if job_id:
|
||||
job = self.jobs.get(job_id, None)
|
||||
return job.status if job is not None else set()
|
||||
return {'VALID'}
|
||||
|
||||
def progress(self, job_id: int) -> float:
|
||||
if job_id:
|
||||
job = self.jobs.get(job_id, None)
|
||||
return job.progress if job is not None else 0.0
|
||||
progress = 0.0
|
||||
nbr_jobs = 0
|
||||
for job in self.jobs.values():
|
||||
if 'RUNNING' in job.status:
|
||||
nbr_jobs += 1
|
||||
progress += job.progress
|
||||
return progress / nbr_jobs if nbr_jobs else 0.0
|
||||
|
||||
def kill(self, job_id: int):
|
||||
self.log.debug('kill(%i)', job_id)
|
||||
if not job_id:
|
||||
for job_id in self.jobs:
|
||||
self.kill(job_id)
|
||||
return
|
||||
|
||||
job = self.jobs.get(job_id, None)
|
||||
if job is not None:
|
||||
job.stop()
|
||||
|
||||
def list_dir(self, job_id: int, asset_list: AssetList) -> int:
|
||||
"""Extends the 'asset_list' object with asset_list for the current dir.
|
||||
|
||||
:param job_id: Job ID of a currently running job (to investigate
|
||||
progress), or zero (0) to start a new job.
|
||||
:param asset_list: AssetList to store directory asset_list in.
|
||||
|
||||
:returns: the job ID, which is the given job ID or a new job ID if a
|
||||
new job was started.
|
||||
"""
|
||||
|
||||
self.log.debug('list_dir(%i), %i entries already loaded', job_id, len(asset_list.entries))
|
||||
|
||||
# TODO: set asset_list.nbr_entries to the total number of entries.
|
||||
|
||||
# job = self.jobs.get(job_id, None)
|
||||
#
|
||||
# asset_list_path = pillar.CloudPath(asset_list.root_path)
|
||||
# if job is not None:
|
||||
# if not isinstance(job, BCloudAssetEngineDirListJob) or job.path != asset_list_path:
|
||||
# # We moved to another directory, abort what's going on now and start a new job.
|
||||
# self.reset()
|
||||
# if not isinstance(job, BCloudAssetEngineDirListJob):
|
||||
# self.log.warn('Job %r is not a BCloudAssetEngineDirListJob', job)
|
||||
# else:
|
||||
# self.log.warn('Job %r is investigating path %r while we want %r', job,
|
||||
# job.path, asset_list_path)
|
||||
# return self._start_dirlist_job(pillar.CloudPath(asset_list_path))
|
||||
#
|
||||
# # Just asking for an update
|
||||
# job.update()
|
||||
# return job_id
|
||||
#
|
||||
# # Moved to another directory, but we haven't started any job yet.
|
||||
# if self.path != asset_list_path:
|
||||
# self.reset()
|
||||
# self.log.info('No job yet, and path changed from %r to %r',
|
||||
# self.path, asset_list_path)
|
||||
# return self._start_dirlist_job(asset_list_path)
|
||||
#
|
||||
# self.log.warn('No job (id=%i), no change in path (%r == %r), nothing to do.', job_id,
|
||||
# self.path, asset_list_path)
|
||||
|
||||
# Just add a fake entry for shits and giggles.
|
||||
if asset_list.nbr_entries == 0:
|
||||
asset_list.nbr_entries = 1
|
||||
|
||||
# import time
|
||||
# time.sleep(1)
|
||||
# The job has been finished; the asset_list is complete.
|
||||
# return job_id
|
||||
return -1
|
||||
|
||||
def load_pre(self, uuids, asset_list: AssetList) -> bool:
|
||||
self.log.debug("load_pre(%r, %r)", uuids, asset_list)
|
||||
return False
|
||||
|
||||
def sort_filter(self, use_sort: bool, use_filter: bool, params: FileSelectParams,
|
||||
asset_list: AssetList) -> bool:
|
||||
self.log.debug("sort_filter(%s, %s, %r, %i in %r)", use_sort, use_filter, params,
|
||||
len(asset_list.entries), asset_list)
|
||||
asset_list.nbr_entries_filtered = asset_list.nbr_entries
|
||||
return False
|
||||
|
||||
def entries_block_get(self, start_index: int, end_index: int, asset_list: AssetList):
|
||||
self.log.debug("entries_block_get(%i, %i, %r)", start_index, end_index, asset_list)
|
||||
|
||||
entry = asset_list.entries.add()
|
||||
entry.name = 'je moeder'
|
||||
entry.description = 'hahaha'
|
||||
entry.type = {'DIR'}
|
||||
entry.relpath = 'relative'
|
||||
entry.uuid = (1, 2, 3, 4)
|
||||
|
||||
variant = entry.variants.add()
|
||||
variant.uuid = (2, 3, 4, 5)
|
||||
variant.name = 'Variant van je moeder'
|
||||
variant.description = 'Variant van je omschrijving'
|
||||
entry.variants.active = variant
|
||||
|
||||
revision = variant.revisions.add()
|
||||
revision.uuid = (3, 4, 5, 6)
|
||||
revision.size = 1024
|
||||
revision.timestamp = time.time()
|
||||
variant.revisions.active = revision
|
||||
|
||||
return True
|
||||
|
||||
def entries_uuid_get(self, uuids: AssetUUIDList, asset_list: AssetList):
|
||||
self.log.debug("entries_uuid_get(%r, %r)", uuids, asset_list)
|
||||
|
||||
for uuid in uuids.uuids:
|
||||
self.entry_from_uuid(asset_list, uuid)
|
||||
return True
|
||||
|
||||
def entry_from_uuid(self, asset_list: AssetList, uuid: AssetUUID):
|
||||
"""Adds the ID'd entry to the asset list.
|
||||
|
||||
Alternatively, it sets the UUID's 'is_unknown_engine' or
|
||||
'is_asset_missing' properties.
|
||||
"""
|
||||
|
||||
uuid_asset = tuple(uuid.uuid_asset)
|
||||
uuid_variant = tuple(uuid.uuid_variant)
|
||||
uuid_revision = tuple(uuid.uuid_revision)
|
||||
|
||||
entry = asset_list.entries.add()
|
||||
entry.name = 'je moeder'
|
||||
entry.description = 'hahaha'
|
||||
entry.type = {'DIR'}
|
||||
entry.relpath = 'relative'
|
||||
entry.uuid = uuid_asset
|
||||
|
||||
variant = entry.variants.add()
|
||||
variant.uuid = uuid_variant
|
||||
variant.name = 'Variant van je moeder'
|
||||
variant.description = 'Variant van je omschrijving'
|
||||
entry.variants.active = variant
|
||||
|
||||
revision = variant.revisions.add()
|
||||
revision.uuid = uuid_revision
|
||||
revision.size = 1024
|
||||
revision.timestamp = time.time()
|
||||
variant.revisions.active = revision
|
||||
|
||||
|
||||
class BCloudPanel:
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
space = context.space_data
|
||||
if space and space.type == 'FILE_BROWSER':
|
||||
ae = space.asset_engine
|
||||
if ae and space.asset_engine_type == "AssetEngineAmber":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class BCloud_PT_options(Panel, BCloudPanel):
|
||||
bl_space_type = 'FILE_BROWSER'
|
||||
bl_region_type = 'TOOLS'
|
||||
bl_category = "Asset Engine"
|
||||
bl_label = "Blender Cloud Options"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
space = context.space_data
|
||||
ae = space.asset_engine
|
||||
|
||||
row = layout.row()
|
||||
|
||||
|
||||
class BCloud_PT_tags(Panel, BCloudPanel):
|
||||
bl_space_type = 'FILE_BROWSER'
|
||||
bl_region_type = 'TOOLS'
|
||||
bl_category = "Filter"
|
||||
bl_label = "Tags"
|
||||
|
||||
def draw(self, context):
|
||||
ae = context.space_data.asset_engine
|
||||
|
||||
# Note: This is *ultra-primitive*!
|
||||
# A good UI will most likely need new widget option anyway (template).
|
||||
# Or maybe just some UIList...
|
||||
# ~ self.layout.props_enum(ae, "tags")
|
||||
# self.layout.template_list("AMBER_UL_tags_filter", "", ae, "tags", ae, "active_tag_index")
|
||||
|
||||
|
||||
def register():
|
||||
import sys
|
||||
import doctest
|
||||
(failures, tests) = doctest.testmod(sys.modules[__name__])
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
if failures:
|
||||
log.warning('There were test failures: %i of %i tests failed.', failures, tests)
|
||||
else:
|
||||
log.debug('All %i tests were successful.', tests)
|
||||
|
||||
bpy.utils.register_class(BCloudAssetEngine)
|
||||
bpy.utils.register_class(BCloud_PT_options)
|
||||
bpy.utils.register_class(BCloud_PT_tags)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.utils.register_class(BCloud_PT_tags)
|
||||
bpy.utils.register_class(BCloud_PT_options)
|
||||
bpy.utils.register_class(BCloudAssetEngine)
|
Reference in New Issue
Block a user