Compare commits

..

20 Commits

Author SHA1 Message Date
ec5f317dac Bumped version to 1.7.0 2017-06-09 11:04:49 +02:00
a51f61d9b5 Added translation: path → path replacement variable 2017-06-09 10:52:56 +02:00
13bc9a89c8 Updated changelog 2017-05-03 15:33:52 +02:00
996b722813 Fixed bug where a symlinked project path caused an issue
Blender would report that the blend file wasn't in the project path, even
though it was. This was caused by resolving symlinks in the project path,
but not in the blendfile path.
2017-05-03 15:33:22 +02:00
e7f2567bfc Clear a LRU cache when (de)activating Flamenco 2017-05-03 15:32:32 +02:00
ff8e71c542 Fixed reloading after upgrading from 1.4.4. 2017-05-03 12:12:58 +02:00
543da5c8d8 Flamenco exclusion filter requires BAM 1.1.7; this is now checked
A warning is shown in the GUI when a BAM version that's too old is used
(instead of simply crashing when an exclusion filter was specified).
2017-05-02 18:48:54 +02:00
01ae0f5f54 Bumped version to 1.6.4 2017-04-21 18:16:14 +02:00
1e80446870 Added file exclusion filter for Flamenco.
A filter like "*.abc;*.mkv;*.mov" can be used to prevent certain files
from being copied to the job storage directory. Requires a Blender that is
bundled with BAM 1.1.7 or newer.
2017-04-21 18:15:59 +02:00
8d5c97931e Fixed capitalisation of label 2017-03-21 14:26:08 +01:00
1a0c00b87a Removed my name from changelog entry 2017-03-21 14:20:15 +01:00
32befc51f8 Include CHANGELOG.md as data file in distribution 2017-03-21 14:18:58 +01:00
06126862d4 Bumped version to 1.6.3 2017-03-21 14:16:37 +01:00
7b8713881e update_version.sh now prints example commit & tag commands 2017-03-21 14:16:29 +01:00
7c65851b75 Allow version passed to update_version.sh to start with "version-" prefix
This allows you to copy-paste a tag and edit it for the new version.
2017-03-21 14:16:07 +01:00
ec72091268 Added changelog, which will contain user-relevant changes. 2017-03-21 14:13:56 +01:00
cf7adb065f Local project path is used by both Attract and Flamenco
It's now shown whenever the project is set up for either one.
2017-03-21 14:06:45 +01:00
74220e4fc4 Bumped version to 1.6.2 2017-03-17 15:39:53 +01:00
0ebd4435e5 Flamenco: when opening non-existing file path, open parent instead
This is very useful for opening render output directories from Blender,
as those will only exist after rendering has started.
2017-03-17 15:08:50 +01:00
c24501661e Fix T50954: Improve Blender Cloud add-on project selector
Attract and Flamenco features are (de)activated based on the extensions
enabled on the selected project. As a result, anyone can use the add-on
again, without seeing Attract or Flamenco things they can't use.
2017-03-17 15:08:09 +01:00
11 changed files with 540 additions and 98 deletions

47
CHANGELOG.md Normal file
View File

@@ -0,0 +1,47 @@
# Blender Cloud changelog
## Version 1.7.0 (2017-06-09)
- Fixed reloading after upgrading from 1.4.4 (our last public release).
- Fixed bug handling a symlinked project path.
- Added support for Manager-defined path replacement variables.
## Version 1.6.4 (2017-04-21)
- Added file exclusion filter for Flamenco. A filter like "*.abc;*.mkv;*.mov" can be
used to prevent certain files from being copied to the job storage directory.
Requires a Blender that is bundled with BAM 1.1.7 or newer.
## Version 1.6.3 (2017-03-21)
- Fixed bug where local project path wasn't shown for projects only set up for Flamenco
(and not Attract).
- Added this CHANGELOG.md file, which will contain user-relevant changes.
## Version 1.6.2 (2017-03-17)
- Flamenco: when opening non-existing file path, open parent instead
- Fix T50954: Improve Blender Cloud add-on project selector
## Version 1.6.1 (2017-03-07)
- Show error in GUI when Blender Cloud is unreachable
- Fixed sample count when using branched path tracing
## Version 1.6.0 (2017-02-14)
- Default to frame chunk size of 1 (instead of 10).
- Turn off "use overwrite" and "use placeholder" for Flamenco blend files.
- Fixed bugs when blendfile is outside the project directory
## Older versions
For the history of older versions, please refer to the
[Git history](https://developer.blender.org/diffusion/BCA/)

View File

@@ -21,7 +21,7 @@
bl_info = {
'name': 'Blender Cloud',
"author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis",
'version': (1, 6, 1),
'version': (1, 7, 0),
'blender': (2, 77, 0),
'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 '
@@ -65,21 +65,28 @@ def register():
def reload_mod(name):
modname = '%s.%s' % (__name__, name)
module = importlib.reload(sys.modules[modname])
sys.modules[modname] = module
return module
try:
old_module = sys.modules[modname]
except KeyError:
# Wasn't loaded before -- can happen after an upgrade.
new_module = importlib.import_module(modname)
else:
new_module = importlib.reload(old_module)
sys.modules[modname] = new_module
return new_module
reload_mod('blendfile')
reload_mod('home_project')
reload_mod('utils')
blender = reload_mod('blender')
async_loop = reload_mod('async_loop')
flamenco = reload_mod('flamenco')
attract = reload_mod('attract')
texture_browser = reload_mod('texture_browser')
settings_sync = reload_mod('settings_sync')
image_sharing = reload_mod('image_sharing')
attract = reload_mod('attract')
flamenco = reload_mod('flamenco')
blender = reload_mod('blender')
else:
from . import (blender, texture_browser, async_loop, settings_sync, blendfile, home_project,
image_sharing, attract, flamenco)
@@ -88,11 +95,13 @@ def register():
async_loop.register()
flamenco.register()
attract.register()
texture_browser.register()
blender.register()
settings_sync.register()
image_sharing.register()
attract.register()
blender.register()
blender.handle_project_update()
def _monkey_patch_requests():

View File

@@ -60,6 +60,9 @@ from bpy.types import Operator, Panel, AddonPreferences
log = logging.getLogger(__name__)
# Global flag used to determine whether panels etc. can be drawn.
attract_is_active = False
def active_strip(context):
try:
@@ -139,6 +142,9 @@ def shot_id_use(strips):
def compute_strip_conflicts(scene):
"""Sets the strip property atc_object_id_conflict for each strip."""
if not attract_is_active:
return
if not scene or not scene.sequence_editor or not scene.sequence_editor.sequences_all:
return
@@ -161,7 +167,13 @@ def scene_update_post_handler(scene):
compute_strip_conflicts(scene)
class ToolsPanel(Panel):
class AttractPollMixin:
@classmethod
def poll(cls, context):
return attract_is_active
class ToolsPanel(AttractPollMixin, Panel):
bl_label = 'Attract'
bl_space_type = 'SEQUENCE_EDITOR'
bl_region_type = 'UI'
@@ -238,7 +250,7 @@ class ToolsPanel(Panel):
layout.operator(ATTRACT_OT_submit_all.bl_idname)
class AttractOperatorMixin:
class AttractOperatorMixin(AttractPollMixin):
"""Mix-in class for all Attract operators."""
def _project_needs_setup_error(self):
@@ -261,7 +273,7 @@ class AttractOperatorMixin:
from .. import pillar, blender
prefs = blender.preferences()
project = self.find_project(prefs.attract_project.project)
project = self.find_project(prefs.project.project)
# FIXME: Eve doesn't seem to handle the $elemMatch projection properly,
# even though it works fine in MongoDB itself. As a result, we have to
@@ -295,7 +307,7 @@ class AttractOperatorMixin:
'cut_in_timeline_in_frames': strip.frame_final_start},
'order': 0,
'node_type': 'attract_shot',
'project': blender.preferences().attract_project.project,
'project': blender.preferences().project.project,
'user': user_uuid}
# Create a Node item with the attract API
@@ -373,7 +385,7 @@ class AttractShotFetchUpdate(AttractOperatorMixin, Operator):
@classmethod
def poll(cls, context):
return any(selected_shots(context))
return AttractOperatorMixin.poll(context) and any(selected_shots(context))
def execute(self, context):
for strip in selected_shots(context):
@@ -393,6 +405,9 @@ class AttractShotRelink(AttractShotFetchUpdate):
@classmethod
def poll(cls, context):
if not AttractOperatorMixin.poll(context):
return False
strip = active_strip(context)
return strip is not None and not getattr(strip, 'atc_object_id', None)
@@ -433,7 +448,8 @@ class ATTRACT_OT_shot_open_in_browser(AttractOperatorMixin, Operator):
@classmethod
def poll(cls, context):
return bool(context.selected_sequences and active_strip(context))
return AttractOperatorMixin.poll(context) and \
bool(context.selected_sequences and active_strip(context))
def execute(self, context):
from ..blender import PILLAR_WEB_SERVER_URL
@@ -459,7 +475,8 @@ class AttractShotDelete(AttractOperatorMixin, Operator):
@classmethod
def poll(cls, context):
return bool(context.selected_sequences)
return AttractOperatorMixin.poll(context) and \
bool(context.selected_sequences)
def execute(self, context):
from .. import pillar
@@ -511,7 +528,8 @@ class AttractStripUnlink(AttractOperatorMixin, Operator):
@classmethod
def poll(cls, context):
return bool(context.selected_sequences)
return AttractOperatorMixin.poll(context) and \
bool(context.selected_sequences)
def execute(self, context):
unlinked_ids = set()
@@ -554,7 +572,8 @@ class AttractShotSubmitSelected(AttractOperatorMixin, Operator):
@classmethod
def poll(cls, context):
return bool(context.selected_sequences)
return AttractOperatorMixin.poll(context) and \
bool(context.selected_sequences)
def execute(self, context):
# Check that the project is set up for Attract.
@@ -610,7 +629,8 @@ class ATTRACT_OT_open_meta_blendfile(AttractOperatorMixin, Operator):
@classmethod
def poll(cls, context):
return bool(any(cls.filename_from_metadata(s) for s in context.selected_sequences))
return AttractOperatorMixin.poll(context) and \
bool(any(cls.filename_from_metadata(s) for s in context.selected_sequences))
@staticmethod
def filename_from_metadata(strip):
@@ -677,7 +697,7 @@ class ATTRACT_OT_make_shot_thumbnail(AttractOperatorMixin,
@classmethod
def poll(cls, context):
return bool(context.selected_sequences)
return AttractOperatorMixin.poll(context) and bool(context.selected_sequences)
@contextlib.contextmanager
def thumbnail_render_settings(self, context, thumbnail_width=512):
@@ -843,7 +863,7 @@ class ATTRACT_OT_make_shot_thumbnail(AttractOperatorMixin,
from .. import blender
prefs = blender.preferences()
project = self.find_project(prefs.attract_project.project)
project = self.find_project(prefs.project.project)
self.log.info('Uploading file %s', filename)
resp = await pillar.pillar_call(
@@ -873,7 +893,8 @@ class ATTRACT_OT_copy_id_to_clipboard(AttractOperatorMixin, Operator):
@classmethod
def poll(cls, context):
return bool(context.selected_sequences and active_strip(context))
return AttractOperatorMixin.poll(context) and \
bool(context.selected_sequences and active_strip(context))
def execute(self, context):
strip = active_strip(context)
@@ -905,6 +926,29 @@ def draw_strip_movie_meta(self, context):
box.label('Original Frame Range: %s-%s' % (sfra, efra))
def activate():
global attract_is_active
log.info('Activating Attract')
attract_is_active = True
bpy.app.handlers.scene_update_post.append(scene_update_post_handler)
draw.callback_enable()
def deactivate():
global attract_is_active
log.info('Deactivating Attract')
attract_is_active = False
draw.callback_disable()
try:
bpy.app.handlers.scene_update_post.remove(scene_update_post_handler)
except ValueError:
# This is thrown when scene_update_post_handler does not exist in the handler list.
pass
def register():
bpy.types.Sequence.atc_is_synced = bpy.props.BoolProperty(name="Is Synced")
bpy.types.Sequence.atc_object_id = bpy.props.StringProperty(name="Attract Object ID")
@@ -942,13 +986,9 @@ def register():
bpy.utils.register_class(ATTRACT_OT_make_shot_thumbnail)
bpy.utils.register_class(ATTRACT_OT_copy_id_to_clipboard)
bpy.app.handlers.scene_update_post.append(scene_update_post_handler)
draw.callback_enable()
def unregister():
draw.callback_disable()
bpy.app.handlers.scene_update_post.remove(scene_update_post_handler)
deactivate()
bpy.utils.unregister_module(__name__)
del bpy.types.Sequence.atc_is_synced
del bpy.types.Sequence.atc_object_id

View File

@@ -172,6 +172,11 @@ def callback_disable():
if not cb_handle:
return
bpy.types.SpaceSequenceEditor.draw_handler_remove(cb_handle[0], 'WINDOW')
try:
bpy.types.SpaceSequenceEditor.draw_handler_remove(cb_handle[0], 'WINDOW')
except ValueError:
# Thrown when already removed.
pass
cb_handle.clear()
tag_redraw_all_sequencer_editors()

View File

@@ -20,7 +20,7 @@
Separated from __init__.py so that we can import & run from non-Blender environments.
"""
import functools
import logging
import os.path
@@ -109,13 +109,61 @@ class SyncStatusProperties(PropertyGroup):
def bcloud_available_projects(self, context):
"""Returns the list of items used by BlenderCloudProjectGroup.project EnumProperty."""
attr_proj = preferences().attract_project
projs = attr_proj.available_projects
projs = preferences().project.available_projects
if not projs:
return [('', 'No projects available in your Blender Cloud', '')]
return [(p['_id'], p['name'], '') for p in projs]
@functools.lru_cache(1)
def project_extensions(project_id) -> set:
"""Returns the extensions the project is enabled for.
At the moment of writing these are 'attract' and 'flamenco'.
"""
log.debug('Finding extensions for project %s', project_id)
# We can't use our @property, since the preferences may be loaded from a
# preferences blend file, in which case it is not constructed from Python code.
available_projects = preferences().project.get('available_projects', [])
if not available_projects:
log.debug('No projects available.')
return set()
proj = next((p for p in available_projects
if p['_id'] == project_id), None)
if proj is None:
log.debug('Project %s not found in available projects.', project_id)
return set()
return set(proj.get('enabled_for', ()))
def handle_project_update(_=None, _2=None):
"""Handles changing projects, which may cause extensions to be disabled/enabled.
Ignores arguments so that it can be used as property update callback.
"""
project_id = preferences().project.project
log.info('Updating internal state to reflect extensions enabled on current project %s.',
project_id)
project_extensions.cache_clear()
from blender_cloud import attract, flamenco
attract.deactivate()
flamenco.deactivate()
enabled_for = project_extensions(project_id)
log.info('Project extensions: %s', enabled_for)
if 'attract' in enabled_for:
attract.activate()
if 'flamenco' in enabled_for:
flamenco.activate()
class BlenderCloudProjectGroup(PropertyGroup):
status = EnumProperty(
items=[
@@ -129,7 +177,9 @@ class BlenderCloudProjectGroup(PropertyGroup):
project = EnumProperty(
items=bcloud_available_projects,
name='Cloud project',
description='Which Blender Cloud project to work with')
description='Which Blender Cloud project to work with',
update=handle_project_update
)
# List of projects is stored in 'available_projects' ID property,
# because I don't know how to store a variable list of strings in a proper RNA property.
@@ -140,6 +190,7 @@ class BlenderCloudProjectGroup(PropertyGroup):
@available_projects.setter
def available_projects(self, new_projects):
self['available_projects'] = new_projects
handle_project_update()
class BlenderCloudPreferences(AddonPreferences):
@@ -165,10 +216,11 @@ class BlenderCloudPreferences(AddonPreferences):
default=True
)
# TODO: store local path with the Attract project, so that people
# can switch projects and the local path switches with it.
attract_project = PointerProperty(type=BlenderCloudProjectGroup)
attract_project_local_path = StringProperty(
# TODO: store project-dependent properties with the project, so that people
# can switch projects and the Attract and Flamenco properties switch with it.
project = PointerProperty(type=BlenderCloudProjectGroup)
cloud_project_local_path = StringProperty(
name='Local Project Path',
description='Local path of your Attract project, used to search for blend files; '
'usually best to set to an absolute path',
@@ -176,6 +228,11 @@ class BlenderCloudPreferences(AddonPreferences):
default='//../')
flamenco_manager = PointerProperty(type=flamenco.FlamencoManagerGroup)
flamenco_exclude_filter = StringProperty(
name='File Exclude Filter',
description='Filter like "*.abc;*.mkv" to prevent certain files to be packed '
'into the output directory',
default='')
# TODO: before making Flamenco public, change the defaults to something less Institute-specific.
# NOTE: The assumption is that the workers can also find the files in the same path.
# This assumption is true for the Blender Institute.
@@ -219,8 +276,8 @@ class BlenderCloudPreferences(AddonPreferences):
blender_id_profile = None
else:
blender_id_profile = blender_id.get_active_profile()
if blender_id is None:
msg_icon = 'ERROR'
text = 'This add-on requires Blender ID'
help_text = 'Make sure that the Blender ID add-on is installed and activated'
@@ -286,13 +343,17 @@ class BlenderCloudPreferences(AddonPreferences):
share_box.label('Image Sharing on Blender Cloud', icon_value=icon('CLOUD'))
share_box.prop(self, 'open_browser_after_share')
# Attract stuff
attract_box = layout.box()
self.draw_attract_buttons(attract_box, self.attract_project)
# Project selector
project_box = layout.box()
project_box.enabled = self.project.status in {'NONE', 'IDLE'}
self.draw_project_selector(project_box, self.project)
extensions = project_extensions(self.project.project)
# Flamenco stuff
flamenco_box = layout.box()
self.draw_flamenco_buttons(flamenco_box, self.flamenco_manager, context)
if 'flamenco' in extensions:
flamenco_box = project_box.column()
self.draw_flamenco_buttons(flamenco_box, self.flamenco_manager, context)
def draw_subscribe_button(self, layout):
layout.operator('pillar.subscribe', icon='WORLD')
@@ -328,12 +389,11 @@ class BlenderCloudPreferences(AddonPreferences):
else:
row_pull.label('Cloud Sync is running.')
def draw_attract_buttons(self, attract_box, bcp: BlenderCloudProjectGroup):
attract_row = attract_box.row(align=True)
attract_row.label('Attract', icon_value=icon('CLOUD'))
def draw_project_selector(self, project_box, bcp: BlenderCloudProjectGroup):
project_row = project_box.row(align=True)
project_row.label('Project settings', icon_value=icon('CLOUD'))
attract_row.enabled = bcp.status in {'NONE', 'IDLE'}
row_buttons = attract_row.row(align=True)
row_buttons = project_row.row(align=True)
projects = bcp.available_projects
project = bcp.project
@@ -350,41 +410,72 @@ class BlenderCloudPreferences(AddonPreferences):
else:
row_buttons.label('Fetching available projects.')
attract_box.prop(self, 'attract_project_local_path')
enabled_for = project_extensions(project)
if not project:
return
if not enabled_for:
project_box.label('This project is not set up for Attract or Flamenco')
return
project_box.label('This project is set up for: %s' %
', '.join(sorted(enabled_for)))
# This is only needed when the project is set up for either Attract or Flamenco.
project_box.prop(self, 'cloud_project_local_path',
text='Local Cloud Project Path')
def draw_flamenco_buttons(self, flamenco_box, bcp: flamenco.FlamencoManagerGroup, context):
flamenco_row = flamenco_box.row(align=True)
flamenco_row.label('Flamenco', icon_value=icon('CLOUD'))
from .flamenco import bam_interface
flamenco_row.enabled = bcp.status in {'NONE', 'IDLE'}
row_buttons = flamenco_row.row(align=True)
header_row = flamenco_box.row(align=True)
header_row.label('Flamenco:', icon_value=icon('CLOUD'))
manager_split = flamenco_box.split(0.32, align=True)
manager_split.label('Manager:')
manager_box = manager_split.row(align=True)
if bcp.status in {'NONE', 'IDLE'}:
if not bcp.available_managers or not bcp.manager:
row_buttons.operator('flamenco.managers',
manager_box.operator('flamenco.managers',
text='Find Flamenco Managers',
icon='FILE_REFRESH')
else:
row_buttons.prop(bcp, 'manager', text='Manager')
row_buttons.operator('flamenco.managers',
manager_box.prop(bcp, 'manager', text='')
manager_box.operator('flamenco.managers',
text='',
icon='FILE_REFRESH')
else:
row_buttons.label('Fetching available managers.')
manager_box.label('Fetching available managers.')
path_box = flamenco_box.row(align=True)
path_box.prop(self, 'flamenco_job_file_path')
path_split = flamenco_box.split(0.32, align=True)
path_split.label(text='Job File Path:')
path_box = path_split.row(align=True)
path_box.prop(self, 'flamenco_job_file_path', text='')
props = path_box.operator('flamenco.explore_file_path', text='', icon='DISK_DRIVE')
props.path = self.flamenco_job_file_path
job_output_box = flamenco_box.column(align=True)
path_box = job_output_box.row(align=True)
path_box.prop(self, 'flamenco_job_output_path')
path_split = job_output_box.split(0.32, align=True)
path_split.label(text='Job Output Path:')
path_box = path_split.row(align=True)
path_box.prop(self, 'flamenco_job_output_path', text='')
props = path_box.operator('flamenco.explore_file_path', text='', icon='DISK_DRIVE')
props.path = self.flamenco_job_output_path
job_output_box.prop(self, 'flamenco_job_output_strip_components',
text='Strip Components')
show_warning = bool(self.flamenco_exclude_filter and
not bam_interface.bam_supports_exclude_option())
job_output_box.alert = show_warning
job_output_box.prop(self, 'flamenco_exclude_filter',
icon='ERROR' if show_warning else 'NONE')
if show_warning:
job_output_box.label(
text='Warning, the exclusion filter requires a newer version of Blender!')
job_output_box.alert = False
prop_split = job_output_box.split(0.32, align=True)
prop_split.label('Strip Components:')
prop_split.prop(self, 'flamenco_job_output_strip_components', text='')
from .flamenco import render_output_path
@@ -400,11 +491,6 @@ class BlenderCloudPreferences(AddonPreferences):
flamenco_box.prop(self, 'flamenco_open_browser_after_submit')
# TODO: make a reusable way to select projects, and use that for Attract and Flamenco.
note_box = flamenco_box.column(align=True)
note_box.label('NOTE: For now, Flamenco uses the same project as Attract.')
note_box.label('This will change in a future version of the add-on.')
class PillarCredentialsUpdate(pillar.PillarOperatorMixin,
Operator):
@@ -488,7 +574,7 @@ class PILLAR_OT_projects(async_loop.AsyncModalOperatorMixin,
self.log.info('Going to fetch projects for user %s', self.user_id)
preferences().attract_project.status = 'FETCHING'
preferences().project.status = 'FETCHING'
# Get all projects, except the home project.
projects_user = await pillar_call(
@@ -497,7 +583,8 @@ class PILLAR_OT_projects(async_loop.AsyncModalOperatorMixin,
'category': {'$ne': 'home'}},
'sort': '-_created',
'projection': {'_id': True,
'name': True},
'name': True,
'extension_props': True},
})
projects_shared = await pillar_call(
@@ -506,20 +593,34 @@ class PILLAR_OT_projects(async_loop.AsyncModalOperatorMixin,
'permissions.groups.group': {'$in': self.db_user.groups}},
'sort': '-_created',
'projection': {'_id': True,
'name': True},
'name': True,
'extension_props': True},
})
# We need to convert to regular dicts before storing in ID properties.
# Also don't store more properties than we need.
projects = [{'_id': p['_id'], 'name': p['name']} for p in projects_user['_items']] + \
[{'_id': p['_id'], 'name': p['name']} for p in projects_shared['_items']]
def reduce_properties(project_list):
for p in project_list:
p = p.to_dict()
extension_props = p.get('extension_props', {})
enabled_for = list(extension_props.keys())
preferences().attract_project.available_projects = projects
self._log.debug('Project %r is enabled for %s', p['name'], enabled_for)
yield {
'_id': p['_id'],
'name': p['name'],
'enabled_for': enabled_for,
}
projects = list(reduce_properties(projects_user['_items'])) + \
list(reduce_properties(projects_shared['_items']))
preferences().project.available_projects = projects
self.quit()
def quit(self):
preferences().attract_project.status = 'IDLE'
preferences().project.status = 'IDLE'
super().quit()

View File

@@ -34,6 +34,9 @@ from ..utils import pyside_cache, redraw
log = logging.getLogger(__name__)
# Global flag used to determine whether panels etc. can be drawn.
flamenco_is_active = False
@pyside_cache('manager')
def available_managers(self, context):
@@ -73,8 +76,15 @@ class FlamencoManagerGroup(PropertyGroup):
self['available_managers'] = new_managers
class FlamencoPollMixin:
@classmethod
def poll(cls, context):
return flamenco_is_active
class FLAMENCO_OT_fmanagers(async_loop.AsyncModalOperatorMixin,
pillar.AuthenticatedPillarOperatorMixin,
FlamencoPollMixin,
Operator):
"""Fetches the Flamenco Managers available to the user"""
bl_idname = 'flamenco.managers'
@@ -115,6 +125,7 @@ class FLAMENCO_OT_fmanagers(async_loop.AsyncModalOperatorMixin,
class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
pillar.AuthenticatedPillarOperatorMixin,
FlamencoPollMixin,
Operator):
"""Performs a Blender render on Flamenco."""
bl_idname = 'flamenco.render'
@@ -128,6 +139,9 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
if not await self.authenticate(context):
return
import pillarsdk.exceptions
from .sdk import Manager
from ..pillar import pillar_call
from ..blender import preferences
scene = context.scene
@@ -149,15 +163,26 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
if not outfile:
return
# Create the job at Flamenco Server.
# Fetch Manager for doing path replacement.
self.log.info('Going to fetch manager %s', self.user_id)
prefs = preferences()
manager_id = prefs.flamenco_manager.manager
try:
manager = await pillar_call(Manager.find, manager_id)
except pillarsdk.exceptions.ResourceNotFound:
self.report({'ERROR'}, 'Manager %s not found, refresh your managers in '
'the Blender Cloud add-on settings.' % manager_id)
self.quit()
return
# Create the job at Flamenco Server.
context.window_manager.flamenco_status = 'COMMUNICATING'
settings = {'blender_cmd': '{blender}',
'chunk_size': scene.flamenco_render_fchunk_size,
'filepath': str(outfile),
'filepath': manager.replace_path(outfile),
'frames': scene.flamenco_render_frame_range,
'render_output': str(render_output),
'render_output': manager.replace_path(render_output),
}
# Add extra settings specific to the job type
@@ -176,8 +201,8 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
try:
job_info = await create_job(self.user_id,
prefs.attract_project.project,
prefs.flamenco_manager.manager,
prefs.project.project,
manager_id,
scene.flamenco_render_job_type,
settings,
'Render %s' % filepath.name,
@@ -289,6 +314,8 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
outdir = Path(prefs.flamenco_job_file_path) / unique_dir
outfile = outdir / filepath.name
exclusion_filter = prefs.flamenco_exclude_filter or None
try:
outdir.mkdir(parents=True)
except Exception as ex:
@@ -298,7 +325,7 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
return None, []
try:
missing_sources = await bam_interface.bam_copy(filepath, outfile)
missing_sources = await bam_interface.bam_copy(filepath, outfile, exclusion_filter)
except bam_interface.CommandExecutionError as ex:
self.log.exception('Unable to execute BAM pack')
self.report({'ERROR'}, 'Unable to execute BAM pack: %s' % ex)
@@ -308,7 +335,7 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
return outfile, missing_sources
class FLAMENCO_OT_scene_to_frame_range(Operator):
class FLAMENCO_OT_scene_to_frame_range(FlamencoPollMixin, Operator):
"""Sets the scene frame range as the Flamenco render frame range."""
bl_idname = 'flamenco.scene_to_frame_range'
bl_label = 'Sets the scene frame range as the Flamenco render frame range'
@@ -321,6 +348,7 @@ class FLAMENCO_OT_scene_to_frame_range(Operator):
class FLAMENCO_OT_copy_files(Operator,
FlamencoPollMixin,
async_loop.AsyncModalOperatorMixin):
"""Uses BAM to copy the current blendfile + dependencies to the target directory."""
bl_idname = 'flamenco.copy_files'
@@ -352,8 +380,13 @@ class FLAMENCO_OT_copy_files(Operator,
bpy.context.window_manager.flamenco_status = 'IDLE'
class FLAMENCO_OT_explore_file_path(Operator):
"""Opens the Flamenco job storage path in a file explorer."""
class FLAMENCO_OT_explore_file_path(FlamencoPollMixin,
Operator):
"""Opens the Flamenco job storage path in a file explorer.
If the path cannot be found, this operator tries to open its parent.
"""
bl_idname = 'flamenco.explore_file_path'
bl_label = 'Open in file explorer'
bl_description = __doc__.rstrip('.')
@@ -362,15 +395,30 @@ class FLAMENCO_OT_explore_file_path(Operator):
def execute(self, context):
import platform
import subprocess
import os
import pathlib
# Possibly open a parent of the path
to_open = pathlib.Path(self.path)
while to_open.parent != to_open: # while we're not at the root
if to_open.exists():
break
to_open = to_open.parent
else:
self.report({'ERROR'}, 'Unable to open %s or any of its parents.' % self.path)
return {'CANCELLED'}
to_open = str(to_open)
if platform.system() == "Windows":
os.startfile(self.path)
import os
os.startfile(to_open)
elif platform.system() == "Darwin":
subprocess.Popen(["open", self.path])
import subprocess
subprocess.Popen(["open", to_open])
else:
subprocess.Popen(["xdg-open", self.path])
import subprocess
subprocess.Popen(["xdg-open", to_open])
return {'FINISHED'}
@@ -443,7 +491,13 @@ def _render_output_path(
return None
try:
proj_rel = blend_filepath.parent.relative_to(project_path)
blend_abspath = blend_filepath.resolve().absolute()
except FileNotFoundError:
# Path.resolve() will raise a FileNotFoundError if the path doesn't exist.
return None
try:
proj_rel = blend_abspath.parent.relative_to(project_path)
except ValueError:
return None
@@ -482,7 +536,7 @@ def render_output_path(context, filepath: Path = None) -> typing.Optional[PurePa
filepath = Path(context.blend_data.filepath)
return _render_output_path(
prefs.attract_project_local_path,
prefs.cloud_project_local_path,
filepath,
prefs.flamenco_job_output_strip_components,
prefs.flamenco_job_output_path,
@@ -491,7 +545,7 @@ def render_output_path(context, filepath: Path = None) -> typing.Optional[PurePa
)
class FLAMENCO_PT_render(bpy.types.Panel):
class FLAMENCO_PT_render(bpy.types.Panel, FlamencoPollMixin):
bl_label = "Flamenco Render"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
@@ -556,6 +610,24 @@ class FLAMENCO_PT_render(bpy.types.Panel):
layout.label('Unknown Flamenco status %s' % flamenco_status)
def activate():
"""Activates draw callbacks, menu items etc. for Flamenco."""
global flamenco_is_active
log.info('Activating Flamenco')
flamenco_is_active = True
_render_output_path.cache_clear()
def deactivate():
"""Deactivates draw callbacks, menu items etc. for Flamenco."""
global flamenco_is_active
log.info('Deactivating Flamenco')
flamenco_is_active = False
_render_output_path.cache_clear()
def register():
from ..utils import redraw
@@ -615,6 +687,7 @@ def register():
def unregister():
deactivate()
bpy.utils.unregister_module(__name__)
try:

View File

@@ -1,5 +1,6 @@
"""BAM packing interface for Flamenco."""
import functools
import logging
from pathlib import Path
import typing
@@ -14,7 +15,30 @@ class CommandExecutionError(Exception):
pass
async def bam_copy(base_blendfile: Path, target_blendfile: Path) -> typing.List[Path]:
if 'bam_supports_exclude_option' in locals():
locals()['bam_supports_exclude_option'].cache_clear()
@functools.lru_cache(maxsize=1)
def bam_supports_exclude_option() -> bool:
"""Returns True if the version of BAM bundled with Blender supports --exclude.
This feature was added to BAM 1.1.7, so we can do a simple version check.
"""
try:
import io_blend_utils
except ImportError:
# If this happens, BAM won't work at all. However, this function can be called from
# the GUI; by being a bit careful while importing, we avoid breaking Blender's GUI.
log.exception('Error importing io_blend_utils module.')
return False
return io_blend_utils.bl_info['version'] >= (1, 1, 7)
async def bam_copy(base_blendfile: Path, target_blendfile: Path,
exclusion_filter: str) -> typing.List[Path]:
"""Uses BAM to copy the given file and dependencies to the target blendfile.
Due to the way blendfile_pack.py is programmed/structured, we cannot import it
@@ -41,6 +65,13 @@ async def bam_copy(base_blendfile: Path, target_blendfile: Path) -> typing.List[
'--mode', 'FILE',
]
if exclusion_filter:
if bam_supports_exclude_option():
args.extend(['--exclude', exclusion_filter])
else:
log.warning('Your version of Blender does not support the exclusion filter, '
'copying all files.')
cmd_to_log = ' '.join(shlex.quote(s) for s in args)
log.info('Executing %s', cmd_to_log)

View File

@@ -1,9 +1,51 @@
import functools
import pathlib
from pillarsdk.resource import List, Find, Create
class Manager(List, Find):
"""Manager class wrapping the REST nodes endpoint"""
path = 'flamenco/managers'
PurePlatformPath = pathlib.PurePath
@functools.lru_cache()
def _sorted_path_replacements(self) -> list:
import sys
if self.path_replacement is None:
return []
print('SORTING PATH REPLACEMENTS')
items = self.path_replacement.to_dict().items()
def by_length(item):
return -len(item[0]), item[0]
platform = sys.platform
return [(varname, platform_replacements[platform])
for varname, platform_replacements in sorted(items, key=by_length)]
def replace_path(self, some_path: pathlib.PurePath) -> str:
"""Performs path variable replacement.
Tries to find platform-specific path prefixes, and replaces them with
variables.
"""
for varname, path in self._sorted_path_replacements():
replacement = self.PurePlatformPath(path)
try:
relpath = some_path.relative_to(replacement)
except ValueError:
# Not relative to each other, so no replacement possible
continue
replacement_root = self.PurePlatformPath('{%s}' % varname)
return (replacement_root / relpath).as_posix()
return some_path.as_posix()
class Job(List, Find, Create):

View File

@@ -227,11 +227,11 @@ setup(
'wheels': BuildWheels},
name='blender_cloud',
description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.',
version='1.6.1',
version='1.7.0',
author='Sybren A. Stüvel',
author_email='sybren@stuvel.eu',
packages=find_packages('.'),
data_files=[('blender_cloud', ['README.md', 'README-flamenco.md']),
data_files=[('blender_cloud', ['README.md', 'README-flamenco.md', 'CHANGELOG.md']),
('blender_cloud/icons', glob.glob('blender_cloud/icons/*'))],
scripts=[],
url='https://developer.blender.org/diffusion/BCA/',

View File

@@ -0,0 +1,90 @@
"""Unittests for blender_cloud.utils.
This unittest requires bpy to be importable, so build Blender as a module and install it
into your virtualenv. See https://stuvel.eu/files/bconf2016/#/10 for notes how.
"""
import datetime
import pathlib
import unittest.mock
import pillarsdk.utils
from blender_cloud.flamenco import sdk
class PathReplacementTest(unittest.TestCase):
def setUp(self):
self.test_manager = sdk.Manager({
'_created': datetime.datetime(2017, 5, 31, 15, 12, 32, tzinfo=pillarsdk.utils.utc),
'_etag': 'c39942ee4bcc4658adcc21e4bcdfb0ae',
'_id': '592edd609837732a2a272c62',
'_updated': datetime.datetime(2017, 6, 8, 14, 51, 3, tzinfo=pillarsdk.utils.utc),
'description': 'Manager formerly known as "testman"',
'job_types': {'sleep': {'vars': {}}},
'name': '<script>alert("this is a manager")</script>',
'owner': '592edd609837732a2a272c63',
'path_replacement': {'job_storage': {'darwin': '/Volume/shared',
'linux': '/shared',
'windows': 's:/'},
'render': {'darwin': '/Volume/render/',
'linux': '/render/',
'windows': 'r:/'},
'longrender': {'darwin': '/Volume/render/long',
'linux': '/render/long',
'windows': 'r:/long'},
},
'projects': ['58cbdd5698377322d95eb55e'],
'service_account': '592edd609837732a2a272c60',
'stats': {'nr_of_workers': 3},
'url': 'http://192.168.3.101:8083/',
'user_groups': ['58cbdd5698377322d95eb55f'],
'variables': {'blender': {'darwin': '/opt/myblenderbuild/blender',
'linux': '/home/sybren/workspace/build_linux/bin/blender '
'--enable-new-depsgraph --factory-startup',
'windows': 'c:/temp/blender.exe'}}}
)
def test_linux(self):
# (expected result, input)
test_paths = [
('/doesnotexistreally', '/doesnotexistreally'),
('{render}/agent327/scenes/A_01_03_B', '/render/agent327/scenes/A_01_03_B'),
('{job_storage}/render/agent327/scenes', '/shared/render/agent327/scenes'),
('{longrender}/agent327/scenes', '/render/long/agent327/scenes'),
]
self._do_test(test_paths, 'linux', pathlib.PurePosixPath)
def test_windows(self):
# (expected result, input)
test_paths = [
('c:/doesnotexistreally', 'c:/doesnotexistreally'),
('c:/some/path', r'c:\some\path'),
('{render}/agent327/scenes/A_01_03_B', r'R:\agent327\scenes\A_01_03_B'),
('{render}/agent327/scenes/A_01_03_B', r'r:\agent327\scenes\A_01_03_B'),
('{render}/agent327/scenes/A_01_03_B', r'r:/agent327/scenes/A_01_03_B'),
('{job_storage}/render/agent327/scenes', 's:/render/agent327/scenes'),
('{longrender}/agent327/scenes', 'r:/long/agent327/scenes'),
]
self._do_test(test_paths, 'windows', pathlib.PureWindowsPath)
def test_darwin(self):
# (expected result, input)
test_paths = [
('/Volume/doesnotexistreally', '/Volume/doesnotexistreally'),
('{render}/agent327/scenes/A_01_03_B', r'/Volume/render/agent327/scenes/A_01_03_B'),
('{job_storage}/render/agent327/scenes', '/Volume/shared/render/agent327/scenes'),
('{longrender}/agent327/scenes', '/Volume/render/long/agent327/scenes'),
]
self._do_test(test_paths, 'darwin', pathlib.PurePosixPath)
def _do_test(self, test_paths, platform, pathclass):
self.test_manager.PurePlatformPath = pathclass
with unittest.mock.patch('sys.platform', platform):
for expected_result, input_path in test_paths:
self.assertEqual(expected_result,
self.test_manager.replace_path(pathclass(input_path)),
'for input %s on platform %s' % (input_path, platform))

View File

@@ -1,15 +1,19 @@
#!/bin/bash
if [ -z "$1" ]; then
VERSION="${1/version-}"
if [ -z "$VERSION" ]; then
echo "Usage: $0 new-version" >&2
exit 1
fi
BL_INFO_VER=$(echo "$1" | sed 's/\./, /g')
BL_INFO_VER=$(echo "$VERSION" | sed 's/\./, /g')
sed "s/version='[^']*'/version='$1'/" -i setup.py
sed "s/version='[^']*'/version='$VERSION'/" -i setup.py
sed "s/'version': ([^)]*)/'version': ($BL_INFO_VER)/" -i blender_cloud/__init__.py
git diff
echo
echo "Don't forget to commit!"
echo "Don't forget to commit and tag:"
echo git commit -m \'Bumped version to $VERSION\' setup.py blender_cloud/__init__.py
echo git tag -a version-$VERSION -m \'Tagged version $VERSION\'