diff --git a/blender_cloud/flamenco/__init__.py b/blender_cloud/flamenco/__init__.py index 0aad846..6e2d68a 100644 --- a/blender_cloud/flamenco/__init__.py +++ b/blender_cloud/flamenco/__init__.py @@ -170,7 +170,7 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin, scene = context.scene # Save to a different file, specifically for Flamenco. - context.window_manager.flamenco_status = 'PACKING' + context.window_manager.flamenco_status = 'SAVING' filepath = await self._save_blendfile(context) # Determine where the render output will be stored. @@ -348,7 +348,7 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin, try: outfile, missing_sources = await bat_interface.bat_copy( - filepath, projdir, outdir, exclusion_filter) + bpy.context, filepath, projdir, outdir, exclusion_filter) except bat_interface.FileTransferError as ex: self.log.error('Could not transfer %d files, starting with %s', len(ex.files_remaining), ex.files_remaining[0]) @@ -380,7 +380,10 @@ class FLAMENCO_OT_scene_to_frame_range(FlamencoPollMixin, Operator): class FLAMENCO_OT_copy_files(Operator, FlamencoPollMixin, async_loop.AsyncModalOperatorMixin): - """Uses BAT to copy the current blendfile + dependencies to the target directory.""" + """Uses BAT to copy the current blendfile + dependencies to the target directory. + + This operator is not used directly, but can be useful for testing. + """ bl_idname = 'flamenco.copy_files' bl_label = 'Copy files to target' bl_description = __doc__.rstrip('.') @@ -395,7 +398,8 @@ class FLAMENCO_OT_copy_files(Operator, prefs = preferences() exclusion_filter = (prefs.flamenco_exclude_filter or '').strip() - missing_sources = await bat_interface.bat_copy( + outpath, missing_sources = await bat_interface.bat_copy( + context, Path(context.blend_data.filepath), Path(prefs.cloud_project_local_path), Path(prefs.flamenco_job_file_path), @@ -405,7 +409,8 @@ class FLAMENCO_OT_copy_files(Operator, if missing_sources: names = (ms.name for ms in missing_sources) self.report({'ERROR'}, 'Missing source files: %s' % '; '.join(names)) - + else: + self.report({'INFO'}, 'Written %s' % outpath) self.quit() def quit(self): @@ -684,12 +689,16 @@ class FLAMENCO_PT_render(bpy.types.Panel, FlamencoPollMixin): layout.operator(FLAMENCO_OT_render.bl_idname, text='Render on Flamenco', icon='RENDER_ANIMATION') - elif flamenco_status == 'PACKING': - layout.label('Flamenco is packing your file + dependencies') + elif flamenco_status == 'INVESTIGATING': + layout.label('Investigating your files') elif flamenco_status == 'COMMUNICATING': layout.label('Communicating with Flamenco Server') - else: - layout.label('Unknown Flamenco status %s' % flamenco_status) + + if flamenco_status == 'TRANSFERRING': + layout.prop(context.window_manager, 'flamenco_progress', + text=context.window_manager.flamenco_status_txt) + elif flamenco_status != 'IDLE': + layout.label(context.window_manager.flamenco_status_txt) def activate(): @@ -790,14 +799,32 @@ def register(): bpy.types.WindowManager.flamenco_status = EnumProperty( items=[ ('IDLE', 'IDLE', 'Not doing anything.'), - ('PACKING', 'PACKING', 'BAT-packing all dependencies.'), + ('SAVING', 'SAVING', 'Saving your file.'), + ('INVESTIGATING', 'INVESTIGATING', 'Finding all dependencies.'), + ('TRANSFERRING', 'TRANSFERRING', 'Transferring all dependencies.'), ('COMMUNICATING', 'COMMUNICATING', 'Communicating with Flamenco Server.'), + ('DONE', 'DONE', 'Not doing anything, but doing something earlier.'), ], name='flamenco_status', default='IDLE', description='Current status of the Flamenco add-on', update=redraw) + bpy.types.WindowManager.flamenco_status_txt = StringProperty( + name='Flamenco Status', + default='', + description='Textual description of what Flamenco is doing', + update=redraw) + + bpy.types.WindowManager.flamenco_progress = IntProperty( + name='Flamenco Progress', + default=0, + description='File transfer progress', + subtype='PERCENTAGE', + min=0, + max=100, + update=redraw) + def unregister(): deactivate() diff --git a/blender_cloud/flamenco/bat_interface.py b/blender_cloud/flamenco/bat_interface.py index 4ff0b75..09989ed 100644 --- a/blender_cloud/flamenco/bat_interface.py +++ b/blender_cloud/flamenco/bat_interface.py @@ -3,18 +3,79 @@ import asyncio import logging import typing -from pathlib import Path +import pathlib from blender_asset_tracer import pack +from blender_asset_tracer.pack import progress + from blender_asset_tracer.pack.transfer import FileTransferError +import bpy + log = logging.getLogger(__name__) -async def bat_copy(base_blendfile: Path, - project: Path, - target: Path, - exclusion_filter: str) -> typing.Tuple[Path, typing.Set[Path]]: +class BatProgress(progress.Callback): + """Report progress of BAT Packing to the UI. + + Uses asyncio.run_coroutine_threadsafe() to ensure the UI is only updated + from the main thread. This is required since we run the BAT Pack in a + background thread. + """ + + def __init__(self, context) -> None: + super().__init__() + self.wm = context.window_manager + self.loop = asyncio.get_event_loop() + + def txt(self, msg: str): + """Set a text in a thread-safe way.""" + async def set_text(): + self.wm.flamenco_status_txt = msg + asyncio.run_coroutine_threadsafe(set_text(), loop=self.loop) + + def pack_start(self) -> None: + self.txt('Starting BAT Pack operation') + + def pack_done(self, + output_blendfile: pathlib.Path, + missing_files: typing.Set[pathlib.Path]) -> None: + if missing_files: + self.txt('There were %d missing files' % len(missing_files)) + else: + self.txt('Pack of %s done' % output_blendfile.name) + + def trace_blendfile(self, filename: pathlib.Path) -> None: + """Called for every blendfile opened when tracing dependencies.""" + self.txt('Inspecting %s' % filename.name) + + def trace_asset(self, filename: pathlib.Path) -> None: + if filename.stem == '.blend': + return + self.txt('Found asset %s' % filename.name) + + def rewrite_blendfile(self, orig_filename: pathlib.Path) -> None: + self.txt('Rewriting %s' % orig_filename.name) + + def transfer_file(self, src: pathlib.Path, dst: pathlib.Path) -> None: + self.txt('Transferring %s' % src.name) + + def transfer_file_skipped(self, src: pathlib.Path, dst: pathlib.Path) -> None: + self.txt('Skipped %s' % src.name) + + def transfer_progress(self, total_bytes: int, transferred_bytes: int) -> None: + self.wm.flamenco_progress = 100 * transferred_bytes / total_bytes + + def missing_file(self, filename: pathlib.Path) -> None: + # TODO(Sybren): report missing files in a nice way + pass + + +async def bat_copy(context, + base_blendfile: pathlib.Path, + project: pathlib.Path, + target: pathlib.Path, + exclusion_filter: str) -> typing.Tuple[pathlib.Path, typing.Set[pathlib.Path]]: """Use BAT🦇 to copy the given file and dependencies to the target location. :raises: FileTransferError if a file couldn't be transferred. @@ -23,13 +84,23 @@ async def bat_copy(base_blendfile: Path, loop = asyncio.get_event_loop() + wm = bpy.context.window_manager + with pack.Packer(base_blendfile, project, target) as packer: if exclusion_filter: packer.exclude(*exclusion_filter.split()) + + packer.progress_cb = BatProgress(context) + log.debug('awaiting strategise') + wm.flamenco_status = 'INVESTIGATING' await loop.run_in_executor(None, packer.strategise) + log.debug('awaiting execute') + wm.flamenco_status = 'TRANSFERRING' await loop.run_in_executor(None, packer.execute) + log.debug('done') + wm.flamenco_status = 'DONE' return packer.output_path, packer.missing_files diff --git a/blender_cloud/utils.py b/blender_cloud/utils.py index aca4bfb..68addec 100644 --- a/blender_cloud/utils.py +++ b/blender_cloud/utils.py @@ -99,4 +99,6 @@ def pyside_cache(propname): def redraw(self, context): + if context.area is None: + return context.area.tag_redraw()