Compare commits

...

16 Commits

Author SHA1 Message Date
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
5b77ae50a1 Bumped version to 1.6.1 2017-03-07 11:01:11 +01:00
74958cf217 Show error in GUI when Blender Cloud is unreachable 2017-03-07 11:00:42 +01:00
5026dfc441 Fixed sample count when using branched path tracing
Thanks to dr. Sharybin for the patch.
2017-03-07 11:00:23 +01:00
10 changed files with 334 additions and 86 deletions

40
CHANGELOG.md Normal file
View File

@@ -0,0 +1,40 @@
# Blender Cloud changelog
## 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 = { bl_info = {
'name': 'Blender Cloud', 'name': 'Blender Cloud',
"author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis", "author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis",
'version': (1, 6, 0), 'version': (1, 6, 4),
'blender': (2, 77, 0), 'blender': (2, 77, 0),
'location': 'Addon Preferences panel, and Ctrl+Shift+Alt+A anywhere for texture browser', 'location': 'Addon Preferences panel, and Ctrl+Shift+Alt+A anywhere for texture browser',
'description': 'Texture library browser and Blender Sync. Requires the Blender ID addon ' 'description': 'Texture library browser and Blender Sync. Requires the Blender ID addon '
@@ -94,6 +94,8 @@ def register():
image_sharing.register() image_sharing.register()
attract.register() attract.register()
blender.handle_project_update()
def _monkey_patch_requests(): def _monkey_patch_requests():
"""Monkey-patch old versions of Requests. """Monkey-patch old versions of Requests.

View File

@@ -60,6 +60,9 @@ from bpy.types import Operator, Panel, AddonPreferences
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# Global flag used to determine whether panels etc. can be drawn.
attract_is_active = False
def active_strip(context): def active_strip(context):
try: try:
@@ -139,6 +142,9 @@ def shot_id_use(strips):
def compute_strip_conflicts(scene): def compute_strip_conflicts(scene):
"""Sets the strip property atc_object_id_conflict for each strip.""" """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: if not scene or not scene.sequence_editor or not scene.sequence_editor.sequences_all:
return return
@@ -161,7 +167,13 @@ def scene_update_post_handler(scene):
compute_strip_conflicts(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_label = 'Attract'
bl_space_type = 'SEQUENCE_EDITOR' bl_space_type = 'SEQUENCE_EDITOR'
bl_region_type = 'UI' bl_region_type = 'UI'
@@ -238,7 +250,7 @@ class ToolsPanel(Panel):
layout.operator(ATTRACT_OT_submit_all.bl_idname) layout.operator(ATTRACT_OT_submit_all.bl_idname)
class AttractOperatorMixin: class AttractOperatorMixin(AttractPollMixin):
"""Mix-in class for all Attract operators.""" """Mix-in class for all Attract operators."""
def _project_needs_setup_error(self): def _project_needs_setup_error(self):
@@ -261,7 +273,7 @@ class AttractOperatorMixin:
from .. import pillar, blender from .. import pillar, blender
prefs = blender.preferences() 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, # 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 # 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}, 'cut_in_timeline_in_frames': strip.frame_final_start},
'order': 0, 'order': 0,
'node_type': 'attract_shot', 'node_type': 'attract_shot',
'project': blender.preferences().attract_project.project, 'project': blender.preferences().project.project,
'user': user_uuid} 'user': user_uuid}
# Create a Node item with the attract API # Create a Node item with the attract API
@@ -373,7 +385,7 @@ class AttractShotFetchUpdate(AttractOperatorMixin, Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return any(selected_shots(context)) return AttractOperatorMixin.poll(context) and any(selected_shots(context))
def execute(self, context): def execute(self, context):
for strip in selected_shots(context): for strip in selected_shots(context):
@@ -393,6 +405,9 @@ class AttractShotRelink(AttractShotFetchUpdate):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
if not AttractOperatorMixin.poll(context):
return False
strip = active_strip(context) strip = active_strip(context)
return strip is not None and not getattr(strip, 'atc_object_id', None) 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 @classmethod
def poll(cls, context): 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): def execute(self, context):
from ..blender import PILLAR_WEB_SERVER_URL from ..blender import PILLAR_WEB_SERVER_URL
@@ -459,7 +475,8 @@ class AttractShotDelete(AttractOperatorMixin, Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return bool(context.selected_sequences) return AttractOperatorMixin.poll(context) and \
bool(context.selected_sequences)
def execute(self, context): def execute(self, context):
from .. import pillar from .. import pillar
@@ -511,7 +528,8 @@ class AttractStripUnlink(AttractOperatorMixin, Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return bool(context.selected_sequences) return AttractOperatorMixin.poll(context) and \
bool(context.selected_sequences)
def execute(self, context): def execute(self, context):
unlinked_ids = set() unlinked_ids = set()
@@ -554,7 +572,8 @@ class AttractShotSubmitSelected(AttractOperatorMixin, Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return bool(context.selected_sequences) return AttractOperatorMixin.poll(context) and \
bool(context.selected_sequences)
def execute(self, context): def execute(self, context):
# Check that the project is set up for Attract. # Check that the project is set up for Attract.
@@ -610,7 +629,8 @@ class ATTRACT_OT_open_meta_blendfile(AttractOperatorMixin, Operator):
@classmethod @classmethod
def poll(cls, context): 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 @staticmethod
def filename_from_metadata(strip): def filename_from_metadata(strip):
@@ -677,7 +697,7 @@ class ATTRACT_OT_make_shot_thumbnail(AttractOperatorMixin,
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return bool(context.selected_sequences) return AttractOperatorMixin.poll(context) and bool(context.selected_sequences)
@contextlib.contextmanager @contextlib.contextmanager
def thumbnail_render_settings(self, context, thumbnail_width=512): def thumbnail_render_settings(self, context, thumbnail_width=512):
@@ -843,7 +863,7 @@ class ATTRACT_OT_make_shot_thumbnail(AttractOperatorMixin,
from .. import blender from .. import blender
prefs = blender.preferences() 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) self.log.info('Uploading file %s', filename)
resp = await pillar.pillar_call( resp = await pillar.pillar_call(
@@ -873,7 +893,8 @@ class ATTRACT_OT_copy_id_to_clipboard(AttractOperatorMixin, Operator):
@classmethod @classmethod
def poll(cls, context): 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): def execute(self, context):
strip = active_strip(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)) 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(): def register():
bpy.types.Sequence.atc_is_synced = bpy.props.BoolProperty(name="Is Synced") 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") 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_make_shot_thumbnail)
bpy.utils.register_class(ATTRACT_OT_copy_id_to_clipboard) 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(): def unregister():
draw.callback_disable() deactivate()
bpy.app.handlers.scene_update_post.remove(scene_update_post_handler)
bpy.utils.unregister_module(__name__) bpy.utils.unregister_module(__name__)
del bpy.types.Sequence.atc_is_synced del bpy.types.Sequence.atc_is_synced
del bpy.types.Sequence.atc_object_id del bpy.types.Sequence.atc_object_id

View File

@@ -172,6 +172,11 @@ def callback_disable():
if not cb_handle: if not cb_handle:
return 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() 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. Separated from __init__.py so that we can import & run from non-Blender environments.
""" """
import functools
import logging import logging
import os.path import os.path
@@ -109,13 +109,61 @@ class SyncStatusProperties(PropertyGroup):
def bcloud_available_projects(self, context): def bcloud_available_projects(self, context):
"""Returns the list of items used by BlenderCloudProjectGroup.project EnumProperty.""" """Returns the list of items used by BlenderCloudProjectGroup.project EnumProperty."""
attr_proj = preferences().attract_project projs = preferences().project.available_projects
projs = attr_proj.available_projects
if not projs: if not projs:
return [('', 'No projects available in your Blender Cloud', '')] return [('', 'No projects available in your Blender Cloud', '')]
return [(p['_id'], p['name'], '') for p in projs] 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): class BlenderCloudProjectGroup(PropertyGroup):
status = EnumProperty( status = EnumProperty(
items=[ items=[
@@ -129,7 +177,9 @@ class BlenderCloudProjectGroup(PropertyGroup):
project = EnumProperty( project = EnumProperty(
items=bcloud_available_projects, items=bcloud_available_projects,
name='Cloud project', 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, # 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. # 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 @available_projects.setter
def available_projects(self, new_projects): def available_projects(self, new_projects):
self['available_projects'] = new_projects self['available_projects'] = new_projects
handle_project_update()
class BlenderCloudPreferences(AddonPreferences): class BlenderCloudPreferences(AddonPreferences):
@@ -165,10 +216,11 @@ class BlenderCloudPreferences(AddonPreferences):
default=True default=True
) )
# TODO: store local path with the Attract project, so that people # TODO: store project-dependent properties with the project, so that people
# can switch projects and the local path switches with it. # can switch projects and the Attract and Flamenco properties switch with it.
attract_project = PointerProperty(type=BlenderCloudProjectGroup) project = PointerProperty(type=BlenderCloudProjectGroup)
attract_project_local_path = StringProperty(
cloud_project_local_path = StringProperty(
name='Local Project Path', name='Local Project Path',
description='Local path of your Attract project, used to search for blend files; ' description='Local path of your Attract project, used to search for blend files; '
'usually best to set to an absolute path', 'usually best to set to an absolute path',
@@ -176,6 +228,11 @@ class BlenderCloudPreferences(AddonPreferences):
default='//../') default='//../')
flamenco_manager = PointerProperty(type=flamenco.FlamencoManagerGroup) 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. # 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. # NOTE: The assumption is that the workers can also find the files in the same path.
# This assumption is true for the Blender Institute. # This assumption is true for the Blender Institute.
@@ -219,8 +276,8 @@ class BlenderCloudPreferences(AddonPreferences):
blender_id_profile = None blender_id_profile = None
else: else:
blender_id_profile = blender_id.get_active_profile() blender_id_profile = blender_id.get_active_profile()
if blender_id is None: if blender_id is None:
msg_icon = 'ERROR' msg_icon = 'ERROR'
text = 'This add-on requires Blender ID' text = 'This add-on requires Blender ID'
help_text = 'Make sure that the Blender ID add-on is installed and activated' 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.label('Image Sharing on Blender Cloud', icon_value=icon('CLOUD'))
share_box.prop(self, 'open_browser_after_share') share_box.prop(self, 'open_browser_after_share')
# Attract stuff # Project selector
attract_box = layout.box() project_box = layout.box()
self.draw_attract_buttons(attract_box, self.attract_project) 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 stuff
flamenco_box = layout.box() if 'flamenco' in extensions:
self.draw_flamenco_buttons(flamenco_box, self.flamenco_manager, context) flamenco_box = project_box.column()
self.draw_flamenco_buttons(flamenco_box, self.flamenco_manager, context)
def draw_subscribe_button(self, layout): def draw_subscribe_button(self, layout):
layout.operator('pillar.subscribe', icon='WORLD') layout.operator('pillar.subscribe', icon='WORLD')
@@ -328,12 +389,11 @@ class BlenderCloudPreferences(AddonPreferences):
else: else:
row_pull.label('Cloud Sync is running.') row_pull.label('Cloud Sync is running.')
def draw_attract_buttons(self, attract_box, bcp: BlenderCloudProjectGroup): def draw_project_selector(self, project_box, bcp: BlenderCloudProjectGroup):
attract_row = attract_box.row(align=True) project_row = project_box.row(align=True)
attract_row.label('Attract', icon_value=icon('CLOUD')) project_row.label('Project settings', icon_value=icon('CLOUD'))
attract_row.enabled = bcp.status in {'NONE', 'IDLE'} row_buttons = project_row.row(align=True)
row_buttons = attract_row.row(align=True)
projects = bcp.available_projects projects = bcp.available_projects
project = bcp.project project = bcp.project
@@ -350,41 +410,62 @@ class BlenderCloudPreferences(AddonPreferences):
else: else:
row_buttons.label('Fetching available projects.') 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): def draw_flamenco_buttons(self, flamenco_box, bcp: flamenco.FlamencoManagerGroup, context):
flamenco_row = flamenco_box.row(align=True) header_row = flamenco_box.row(align=True)
flamenco_row.label('Flamenco', icon_value=icon('CLOUD')) header_row.label('Flamenco:', icon_value=icon('CLOUD'))
flamenco_row.enabled = bcp.status in {'NONE', 'IDLE'} manager_split = flamenco_box.split(0.32, align=True)
row_buttons = flamenco_row.row(align=True) manager_split.label('Manager:')
manager_box = manager_split.row(align=True)
if bcp.status in {'NONE', 'IDLE'}: if bcp.status in {'NONE', 'IDLE'}:
if not bcp.available_managers or not bcp.manager: if not bcp.available_managers or not bcp.manager:
row_buttons.operator('flamenco.managers', manager_box.operator('flamenco.managers',
text='Find Flamenco Managers', text='Find Flamenco Managers',
icon='FILE_REFRESH') icon='FILE_REFRESH')
else: else:
row_buttons.prop(bcp, 'manager', text='Manager') manager_box.prop(bcp, 'manager', text='')
row_buttons.operator('flamenco.managers', manager_box.operator('flamenco.managers',
text='', text='',
icon='FILE_REFRESH') icon='FILE_REFRESH')
else: else:
row_buttons.label('Fetching available managers.') manager_box.label('Fetching available managers.')
path_box = flamenco_box.row(align=True) path_split = flamenco_box.split(0.32, align=True)
path_box.prop(self, 'flamenco_job_file_path') 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_box.operator('flamenco.explore_file_path', text='', icon='DISK_DRIVE')
props.path = self.flamenco_job_file_path props.path = self.flamenco_job_file_path
job_output_box = flamenco_box.column(align=True) job_output_box = flamenco_box.column(align=True)
path_box = job_output_box.row(align=True) path_split = job_output_box.split(0.32, align=True)
path_box.prop(self, 'flamenco_job_output_path') 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_box.operator('flamenco.explore_file_path', text='', icon='DISK_DRIVE')
props.path = self.flamenco_job_output_path props.path = self.flamenco_job_output_path
job_output_box.prop(self, 'flamenco_job_output_strip_components', job_output_box.prop(self, 'flamenco_exclude_filter')
text='Strip Components')
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 from .flamenco import render_output_path
@@ -400,11 +481,6 @@ class BlenderCloudPreferences(AddonPreferences):
flamenco_box.prop(self, 'flamenco_open_browser_after_submit') 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, class PillarCredentialsUpdate(pillar.PillarOperatorMixin,
Operator): Operator):
@@ -488,7 +564,7 @@ class PILLAR_OT_projects(async_loop.AsyncModalOperatorMixin,
self.log.info('Going to fetch projects for user %s', self.user_id) 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. # Get all projects, except the home project.
projects_user = await pillar_call( projects_user = await pillar_call(
@@ -497,7 +573,8 @@ class PILLAR_OT_projects(async_loop.AsyncModalOperatorMixin,
'category': {'$ne': 'home'}}, 'category': {'$ne': 'home'}},
'sort': '-_created', 'sort': '-_created',
'projection': {'_id': True, 'projection': {'_id': True,
'name': True}, 'name': True,
'extension_props': True},
}) })
projects_shared = await pillar_call( projects_shared = await pillar_call(
@@ -506,20 +583,34 @@ class PILLAR_OT_projects(async_loop.AsyncModalOperatorMixin,
'permissions.groups.group': {'$in': self.db_user.groups}}, 'permissions.groups.group': {'$in': self.db_user.groups}},
'sort': '-_created', 'sort': '-_created',
'projection': {'_id': True, 'projection': {'_id': True,
'name': True}, 'name': True,
'extension_props': True},
}) })
# We need to convert to regular dicts before storing in ID properties. # We need to convert to regular dicts before storing in ID properties.
# Also don't store more properties than we need. # Also don't store more properties than we need.
projects = [{'_id': p['_id'], 'name': p['name']} for p in projects_user['_items']] + \ def reduce_properties(project_list):
[{'_id': p['_id'], 'name': p['name']} for p in projects_shared['_items']] 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() self.quit()
def quit(self): def quit(self):
preferences().attract_project.status = 'IDLE' preferences().project.status = 'IDLE'
super().quit() super().quit()

View File

@@ -34,6 +34,9 @@ from ..utils import pyside_cache, redraw
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# Global flag used to determine whether panels etc. can be drawn.
flamenco_is_active = False
@pyside_cache('manager') @pyside_cache('manager')
def available_managers(self, context): def available_managers(self, context):
@@ -73,8 +76,15 @@ class FlamencoManagerGroup(PropertyGroup):
self['available_managers'] = new_managers self['available_managers'] = new_managers
class FlamencoPollMixin:
@classmethod
def poll(cls, context):
return flamenco_is_active
class FLAMENCO_OT_fmanagers(async_loop.AsyncModalOperatorMixin, class FLAMENCO_OT_fmanagers(async_loop.AsyncModalOperatorMixin,
pillar.AuthenticatedPillarOperatorMixin, pillar.AuthenticatedPillarOperatorMixin,
FlamencoPollMixin,
Operator): Operator):
"""Fetches the Flamenco Managers available to the user""" """Fetches the Flamenco Managers available to the user"""
bl_idname = 'flamenco.managers' bl_idname = 'flamenco.managers'
@@ -115,6 +125,7 @@ class FLAMENCO_OT_fmanagers(async_loop.AsyncModalOperatorMixin,
class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin, class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
pillar.AuthenticatedPillarOperatorMixin, pillar.AuthenticatedPillarOperatorMixin,
FlamencoPollMixin,
Operator): Operator):
"""Performs a Blender render on Flamenco.""" """Performs a Blender render on Flamenco."""
bl_idname = 'flamenco.render' bl_idname = 'flamenco.render'
@@ -162,7 +173,11 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
# Add extra settings specific to the job type # Add extra settings specific to the job type
if scene.flamenco_render_job_type == 'blender-render-progressive': if scene.flamenco_render_job_type == 'blender-render-progressive':
samples = scene.cycles.samples if scene.cycles.progressive == 'BRANCHED_PATH':
samples = scene.cycles.aa_samples
else:
samples = scene.cycles.samples
if scene.cycles.use_square_samples: if scene.cycles.use_square_samples:
samples **= 2 samples **= 2
@@ -172,7 +187,7 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
try: try:
job_info = await create_job(self.user_id, job_info = await create_job(self.user_id,
prefs.attract_project.project, prefs.project.project,
prefs.flamenco_manager.manager, prefs.flamenco_manager.manager,
scene.flamenco_render_job_type, scene.flamenco_render_job_type,
settings, settings,
@@ -285,6 +300,8 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
outdir = Path(prefs.flamenco_job_file_path) / unique_dir outdir = Path(prefs.flamenco_job_file_path) / unique_dir
outfile = outdir / filepath.name outfile = outdir / filepath.name
exclusion_filter = prefs.flamenco_exclude_filter or None
try: try:
outdir.mkdir(parents=True) outdir.mkdir(parents=True)
except Exception as ex: except Exception as ex:
@@ -294,7 +311,7 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
return None, [] return None, []
try: 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: except bam_interface.CommandExecutionError as ex:
self.log.exception('Unable to execute BAM pack') self.log.exception('Unable to execute BAM pack')
self.report({'ERROR'}, 'Unable to execute BAM pack: %s' % ex) self.report({'ERROR'}, 'Unable to execute BAM pack: %s' % ex)
@@ -304,7 +321,7 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
return outfile, missing_sources 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.""" """Sets the scene frame range as the Flamenco render frame range."""
bl_idname = 'flamenco.scene_to_frame_range' bl_idname = 'flamenco.scene_to_frame_range'
bl_label = 'Sets the scene frame range as the Flamenco render frame range' bl_label = 'Sets the scene frame range as the Flamenco render frame range'
@@ -317,6 +334,7 @@ class FLAMENCO_OT_scene_to_frame_range(Operator):
class FLAMENCO_OT_copy_files(Operator, class FLAMENCO_OT_copy_files(Operator,
FlamencoPollMixin,
async_loop.AsyncModalOperatorMixin): async_loop.AsyncModalOperatorMixin):
"""Uses BAM to copy the current blendfile + dependencies to the target directory.""" """Uses BAM to copy the current blendfile + dependencies to the target directory."""
bl_idname = 'flamenco.copy_files' bl_idname = 'flamenco.copy_files'
@@ -348,8 +366,13 @@ class FLAMENCO_OT_copy_files(Operator,
bpy.context.window_manager.flamenco_status = 'IDLE' bpy.context.window_manager.flamenco_status = 'IDLE'
class FLAMENCO_OT_explore_file_path(Operator): class FLAMENCO_OT_explore_file_path(FlamencoPollMixin,
"""Opens the Flamenco job storage path in a file explorer.""" 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_idname = 'flamenco.explore_file_path'
bl_label = 'Open in file explorer' bl_label = 'Open in file explorer'
bl_description = __doc__.rstrip('.') bl_description = __doc__.rstrip('.')
@@ -358,15 +381,30 @@ class FLAMENCO_OT_explore_file_path(Operator):
def execute(self, context): def execute(self, context):
import platform import platform
import subprocess import pathlib
import os
# 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": if platform.system() == "Windows":
os.startfile(self.path) import os
os.startfile(to_open)
elif platform.system() == "Darwin": elif platform.system() == "Darwin":
subprocess.Popen(["open", self.path]) import subprocess
subprocess.Popen(["open", to_open])
else: else:
subprocess.Popen(["xdg-open", self.path]) import subprocess
subprocess.Popen(["xdg-open", to_open])
return {'FINISHED'} return {'FINISHED'}
@@ -478,7 +516,7 @@ def render_output_path(context, filepath: Path = None) -> typing.Optional[PurePa
filepath = Path(context.blend_data.filepath) filepath = Path(context.blend_data.filepath)
return _render_output_path( return _render_output_path(
prefs.attract_project_local_path, prefs.cloud_project_local_path,
filepath, filepath,
prefs.flamenco_job_output_strip_components, prefs.flamenco_job_output_strip_components,
prefs.flamenco_job_output_path, prefs.flamenco_job_output_path,
@@ -487,7 +525,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_label = "Flamenco Render"
bl_space_type = 'PROPERTIES' bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW' bl_region_type = 'WINDOW'
@@ -552,6 +590,22 @@ class FLAMENCO_PT_render(bpy.types.Panel):
layout.label('Unknown Flamenco status %s' % flamenco_status) 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
def deactivate():
"""Deactivates draw callbacks, menu items etc. for Flamenco."""
global flamenco_is_active
log.info('Deactivating Flamenco')
flamenco_is_active = False
def register(): def register():
from ..utils import redraw from ..utils import redraw
@@ -611,6 +665,7 @@ def register():
def unregister(): def unregister():
deactivate()
bpy.utils.unregister_module(__name__) bpy.utils.unregister_module(__name__)
try: try:

View File

@@ -14,7 +14,8 @@ class CommandExecutionError(Exception):
pass pass
async def bam_copy(base_blendfile: Path, target_blendfile: Path) -> typing.List[Path]: 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. """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 Due to the way blendfile_pack.py is programmed/structured, we cannot import it
@@ -41,6 +42,9 @@ async def bam_copy(base_blendfile: Path, target_blendfile: Path) -> typing.List[
'--mode', 'FILE', '--mode', 'FILE',
] ]
if exclusion_filter:
args.extend(['--exclude', exclusion_filter])
cmd_to_log = ' '.join(shlex.quote(s) for s in args) cmd_to_log = ' '.join(shlex.quote(s) for s in args)
log.info('Executing %s', cmd_to_log) log.info('Executing %s', cmd_to_log)

View File

@@ -860,6 +860,13 @@ class AuthenticatedPillarOperatorMixin(PillarOperatorMixin):
self.report({'ERROR'}, 'Please log in on Blender ID first.') self.report({'ERROR'}, 'Please log in on Blender ID first.')
self.quit() self.quit()
return False return False
except requests.exceptions.ConnectionError:
self.log.exception('Error checking pillar credentials.')
self.report({'ERROR'}, 'Unable to connect to Blender Cloud, '
'check your internet connection.')
self.quit()
return False
self.user_id = self.db_user['_id'] self.user_id = self.db_user['_id']
return True return True

View File

@@ -227,11 +227,11 @@ setup(
'wheels': BuildWheels}, 'wheels': BuildWheels},
name='blender_cloud', name='blender_cloud',
description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.', description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.',
version='1.6.0', version='1.6.4',
author='Sybren A. Stüvel', author='Sybren A. Stüvel',
author_email='sybren@stuvel.eu', author_email='sybren@stuvel.eu',
packages=find_packages('.'), packages=find_packages('.'),
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/*'))], ('blender_cloud/icons', glob.glob('blender_cloud/icons/*'))],
scripts=[], scripts=[],
url='https://developer.blender.org/diffusion/BCA/', url='https://developer.blender.org/diffusion/BCA/',

View File

@@ -1,15 +1,19 @@
#!/bin/bash #!/bin/bash
if [ -z "$1" ]; then VERSION="${1/version-}"
if [ -z "$VERSION" ]; then
echo "Usage: $0 new-version" >&2 echo "Usage: $0 new-version" >&2
exit 1 exit 1
fi 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 sed "s/'version': ([^)]*)/'version': ($BL_INFO_VER)/" -i blender_cloud/__init__.py
git diff git diff
echo 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\'