Compare commits

..

29 Commits

Author SHA1 Message Date
68b046c714 Bumped version to 1.5.2 2017-01-06 17:01:58 +01:00
cb20d6ee03 Fixed icon, the MOVE_UP_VEC icon was removed
This happened in Blender commit rBb2159b94bcd6c4e30c47b9970601e6f2ca2a0750
2017-01-06 17:01:06 +01:00
Dalai Felinto
645bdd950f Attract: prevent ui/console errors when no strip exists 2016-12-06 22:00:58 +01:00
Dalai Felinto
74a5830dae setup fdist option for files distribution (unzipped files)
This is the equivalent of `python setup.py bdist` and unzipping the outputted file. If no --dest-path is specified it dumps the files in the same folder as the .zip.

It is used for symlinking of the latest addon in production computers without having to deal with .zip files.

Review, design, lessons, patient orientations by Sybren Stüvel
2016-11-11 16:15:24 +01:00
da4d4df5fb Bumped version to 1.5.1 2016-11-11 09:28:10 +01:00
9c3098cc0d Attract: Added some poll methods 2016-11-11 09:26:52 +01:00
98beaf7fb7 Unlinking a single shot copies the shot ID to the clipboard.
This allows you to easily unlink one strip, and relink another. This cannot
be done for multiple shots simultaneously.
2016-11-11 09:23:39 +01:00
3364371ac6 Use '{nr of shots} Shots' instead of 'Selected Shots'
This is a bit shorter, and more concrete.
2016-11-11 09:22:59 +01:00
2723b07fa2 Nicer button layout for unlink & delete
This makes 'delete' harder to hit accidentally.
2016-11-11 09:17:25 +01:00
c2a037ca89 Added button to copy a shot ID to the clipboard 2016-11-11 09:15:04 +01:00
d3451d4de3 Icon tweaks 2016-11-11 09:14:51 +01:00
5094977614 Attract: "delete shots" now works on all selected Attract strips 2016-11-11 09:06:59 +01:00
Dalai Felinto
a11a55be22 Implement attract "Trim End" 2016-11-10 18:49:13 +01:00
68d2fc8e42 Fixed scene update post handler disappearing. 2016-11-08 15:11:27 +01:00
2de4a8e87c Removed beta warning 2016-11-08 14:09:33 +01:00
39b2bacdcc Bumped version to 1.5.0 2016-11-08 14:08:15 +01:00
a1416f99dd Added script to bump versions in all the right places.
Must be called with major.minor.micro revision number (so 3 components).

Signed-off-by: Sybren A. Stüvel <sybren@stuvel.eu>
2016-11-08 14:08:03 +01:00
0fa7d60028 Keep Attract & Image Share prefs enabled when sync error is shown. 2016-11-08 10:14:26 +01:00
c1b6480f9a Texture browser: Also draw a dark background for INITIALIZING state 2016-11-08 10:14:26 +01:00
56353d4177 More captialisation of button labels. 2016-11-07 13:39:33 +01:00
469a9318af Also allow meta strips to be considered "shots" in Attract. 2016-11-07 13:39:14 +01:00
e265081131 Change wiki URL to HTTPS 2016-11-07 11:23:30 +01:00
115eea82c6 Prevent errors when there is no sequencer in the current scene 2016-11-04 17:47:27 +01:00
900068a6f5 Slight improvements to Attract shot delete operator 2016-11-04 17:47:13 +01:00
c8229500d1 Bugfix 2016-11-04 16:42:26 +01:00
65ff9da428 Attract: Draw conflicts more subtly 2016-11-04 16:12:13 +01:00
fcba8a2e0f Attract: Compute strip conflicts in scene update handler 2016-11-04 16:11:59 +01:00
7ef5e522f8 Attract thumbs: only use middle frame when current frame not on shot
This now also applies when rendering multiple shot thumbnails.
2016-11-04 15:45:23 +01:00
ae570e5907 Allow undeletion of shots by relinking to the edit. 2016-11-04 13:44:12 +01:00
7 changed files with 208 additions and 50 deletions

View File

@@ -21,15 +21,14 @@
bl_info = {
'name': 'Blender Cloud',
"author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis",
'version': (1, 4, 999),
'version': (1, 5, 2),
'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 '
'and Blender 2.77a or newer.',
'wiki_url': 'http://wiki.blender.org/index.php/Extensions:2.6/Py/'
'wiki_url': 'https://wiki.blender.org/index.php/Extensions:2.6/Py/'
'Scripts/System/BlenderCloud',
'category': 'System',
'warning': 'This is a beta version; the first to support Attract.'
}
import logging

View File

@@ -70,8 +70,12 @@ def active_strip(context):
def selected_shots(context):
"""Generator, yields selected strips if they are Attract shots."""
selected_sequences = context.selected_sequences
for strip in context.selected_sequences:
if selected_sequences is None:
return
for strip in selected_sequences:
atc_object_id = getattr(strip, 'atc_object_id')
if not atc_object_id:
continue
@@ -81,6 +85,11 @@ def selected_shots(context):
def all_shots(context):
"""Generator, yields all strips if they are Attract shots."""
sequence_editor = context.scene.sequence_editor
if sequence_editor is None:
# we should throw an exception, but at least this change prevents an error
return []
for strip in context.scene.sequence_editor.sequences_all:
atc_object_id = getattr(strip, 'atc_object_id')
@@ -127,18 +136,31 @@ def shot_id_use(strips):
return ids_in_use
def compute_strip_conflicts(context):
def compute_strip_conflicts(scene):
"""Sets the strip property atc_object_id_conflict for each strip."""
ids_in_use = shot_id_use(context.scene.sequence_editor.sequences_all)
if not scene or not scene.sequence_editor or not scene.sequence_editor.sequences_all:
return
tag_redraw = False
ids_in_use = shot_id_use(scene.sequence_editor.sequences_all)
for strips in ids_in_use.values():
is_conflict = len(strips) > 1
for strip in strips:
if strip.atc_object_id_conflict != is_conflict:
tag_redraw = True
strip.atc_object_id_conflict = is_conflict
if tag_redraw:
draw.tag_redraw_all_sequencer_editors()
return ids_in_use
@bpy.app.handlers.persistent
def scene_update_post_handler(scene):
compute_strip_conflicts(scene)
class ToolsPanel(Panel):
bl_label = 'Attract'
bl_space_type = 'SEQUENCE_EDITOR'
@@ -152,12 +174,12 @@ class ToolsPanel(Panel):
def draw(self, context):
strip = active_strip(context)
layout = self.layout
strip_types = {'MOVIE', 'IMAGE'}
strip_types = {'MOVIE', 'IMAGE', 'META'}
selshots = list(selected_shots(context))
if strip and strip.type in strip_types and strip.atc_object_id:
if len(selshots) > 1:
noun = 'Selected Shots'
noun = '%i Shots' % len(selshots)
else:
noun = 'This Shot'
@@ -180,26 +202,35 @@ class ToolsPanel(Panel):
sub = layout.column(align=True)
row = sub.row(align=True)
if bpy.ops.attract.submit_selected.poll():
row.operator('attract.submit_selected', text='Submit %s' % noun)
row.operator('attract.submit_selected',
text='Submit %s' % noun,
icon='TRIA_UP')
else:
row.operator(ATTRACT_OT_submit_all.bl_idname)
row.operator(AttractShotFetchUpdate.bl_idname,
text='', icon='FILE_REFRESH')
row.operator(ATTRACT_OT_shot_open_in_browser.bl_idname,
text='', icon='WORLD')
row.operator(ATTRACT_OT_copy_id_to_clipboard.bl_idname,
text='', icon='COPYDOWN')
sub.operator(ATTRACT_OT_make_shot_thumbnail.bl_idname,
text='Render Thumbnail for %s' % noun)
text='Render Thumbnail for %s' % noun,
icon='RENDER_STILL')
# Group more dangerous operations.
dangerous_sub = layout.column(align=True)
dangerous_sub.operator(AttractShotDelete.bl_idname)
dangerous_sub.operator('attract.strip_unlink')
dangerous_sub = layout.split(0.6, align=True)
dangerous_sub.operator('attract.strip_unlink',
text='Unlink %s' % noun,
icon='PANEL_CLOSE')
dangerous_sub.operator(AttractShotDelete.bl_idname,
text='Delete %s' % noun,
icon='CANCEL')
elif context.selected_sequences:
if len(context.selected_sequences) > 1:
noun = 'selected strips'
noun = 'Selected Strips'
else:
noun = 'this strip'
noun = 'This Strip'
layout.operator(AttractShotSubmitSelected.bl_idname,
text='Submit %s as New Shot' % noun)
layout.operator('attract.shot_relink')
@@ -259,6 +290,7 @@ class AttractOperatorMixin:
'notes': '',
'used_in_edit': True,
'trim_start_in_frames': strip.frame_offset_start,
'trim_end_in_frames': strip.frame_offset_end,
'duration_in_edit_in_frames': strip.frame_final_duration,
'cut_in_timeline_in_frames': strip.frame_final_start},
'order': 0,
@@ -282,7 +314,6 @@ class AttractOperatorMixin:
strip.atc_notes = node['properties']['notes']
strip.atc_status = node['properties']['status']
compute_strip_conflicts(bpy.context)
draw.tag_redraw_all_sequencer_editors()
def submit_update(self, strip):
@@ -294,6 +325,7 @@ class AttractOperatorMixin:
'$set': {
'name': strip.atc_name,
'properties.trim_start_in_frames': strip.frame_offset_start,
'properties.trim_end_in_frames': strip.frame_offset_end,
'properties.duration_in_edit_in_frames': strip.frame_final_duration,
'properties.cut_in_timeline_in_frames': strip.frame_final_start,
'properties.status': strip.atc_status,
@@ -308,6 +340,11 @@ class AttractOperatorMixin:
def relink(self, strip, atc_object_id, *, refresh=False):
from .. import pillar
# The node may have been deleted, so we need to send a 'relink' before we try
# to fetch the node itself.
node = Node({'_id': atc_object_id})
pillar.sync_call(node.patch, {'op': 'relink'})
try:
node = pillar.sync_call(Node.find, atc_object_id, caching=False)
except (sdk_exceptions.ResourceNotFound, sdk_exceptions.MethodNotAllowed):
@@ -317,7 +354,6 @@ class AttractOperatorMixin:
strip.atc_is_synced = False
return {'CANCELLED'}
pillar.sync_call(node.patch, {'op': 'relink'})
strip.atc_is_synced = True
if not refresh:
strip.atc_name = node.name
@@ -327,8 +363,6 @@ class AttractOperatorMixin:
strip.atc_status = node.properties.status
strip.atc_notes = node.properties.notes or ''
strip.atc_description = node.description or ''
compute_strip_conflicts(bpy.context)
draw.tag_redraw_all_sequencer_editors()
@@ -397,6 +431,10 @@ class ATTRACT_OT_shot_open_in_browser(AttractOperatorMixin, Operator):
bl_label = 'Open in Browser'
bl_description = 'Opens a webbrowser to show the shot on Attract'
@classmethod
def poll(cls, context):
return bool(context.selected_sequences and active_strip(context))
def execute(self, context):
from ..blender import PILLAR_WEB_SERVER_URL
import webbrowser
@@ -419,6 +457,10 @@ class AttractShotDelete(AttractOperatorMixin, Operator):
confirm = bpy.props.BoolProperty(name='confirm')
@classmethod
def poll(cls, context):
return bool(context.selected_sequences)
def execute(self, context):
from .. import pillar
@@ -426,23 +468,40 @@ class AttractShotDelete(AttractOperatorMixin, Operator):
self.report({'WARNING'}, 'Delete aborted.')
return {'CANCELLED'}
strip = active_strip(context)
node = pillar.sync_call(Node.find, strip.atc_object_id)
if not pillar.sync_call(node.delete):
print('Unable to delete the strip node on Attract.')
return {'CANCELLED'}
removed = kept = 0
for strip in selected_shots(context):
node = pillar.sync_call(Node.find, strip.atc_object_id)
if not pillar.sync_call(node.delete):
self.report({'ERROR'}, 'Unable to delete shot %s on Attract.' % strip.atc_name)
kept += 1
continue
remove_atc_props(strip)
removed += 1
if kept:
self.report({'ERROR'}, 'Removed %i shots, but was unable to remove %i' %
(removed, kept))
else:
self.report({'INFO'}, 'Removed all %i shots from Attract' % removed)
remove_atc_props(strip)
draw.tag_redraw_all_sequencer_editors()
return {'FINISHED'}
def invoke(self, context, event):
self.confirm = False
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, 'confirm', text="I hereby confirm I want to delete this shot from The Edit.")
selshots = list(selected_shots(context))
if len(selshots) > 1:
noun = '%i shots' % len(selshots)
else:
noun = 'this shot'
col.prop(self, 'confirm', text="I hereby confirm: delete %s from The Edit." % noun)
class AttractStripUnlink(AttractOperatorMixin, Operator):
@@ -450,6 +509,10 @@ class AttractStripUnlink(AttractOperatorMixin, Operator):
bl_label = 'Unlink Shot From This Strip'
bl_description = 'Remove Attract props from the selected strip(s)'
@classmethod
def poll(cls, context):
return bool(context.selected_sequences)
def execute(self, context):
unlinked_ids = set()
@@ -464,7 +527,7 @@ class AttractStripUnlink(AttractOperatorMixin, Operator):
# For all Object IDs that are no longer in use in the edit, let Attract know.
# This should be done with care, as the shot could have been attached to multiple
# strips.
id_to_shots = compute_strip_conflicts(context)
id_to_shots = compute_strip_conflicts(context.scene)
for oid in unlinked_ids:
if len(id_to_shots[oid]):
# Still in use
@@ -472,7 +535,13 @@ class AttractStripUnlink(AttractOperatorMixin, Operator):
node = Node({'_id': oid})
pillar.sync_call(node.patch, {'op': 'unlink'})
self.report({'INFO'}, 'Shot %s has been marked as Unused.' % oid)
if len(unlinked_ids) == 1:
shot_id = unlinked_ids.pop()
context.window_manager.clipboard = shot_id
self.report({'INFO'}, 'Copied unlinked shot ID %s to clipboard' % shot_id)
else:
self.report({'INFO'}, '%i shots have been marked as Unused.' % len(unlinked_ids))
draw.tag_redraw_all_sequencer_editors()
return {'FINISHED'}
@@ -639,9 +708,13 @@ class ATTRACT_OT_make_shot_thumbnail(AttractOperatorMixin,
@contextlib.contextmanager
def temporary_current_frame(self, context):
"""Allows the context to set the scene current frame, restores it on exit.
Yields the initial current frame, so it can be used for reference in the context.
"""
current_frame = context.scene.frame_current
try:
yield
yield current_frame
finally:
context.scene.frame_current = current_frame
@@ -649,19 +722,25 @@ class ATTRACT_OT_make_shot_thumbnail(AttractOperatorMixin,
nr_of_strips = len(context.selected_sequences)
do_multishot = nr_of_strips > 1
with self.temporary_current_frame(context):
with self.temporary_current_frame(context) as original_curframe:
# The multishot and singleshot branches do pretty much the same thing,
# but report differently to the user.
if do_multishot:
context.window_manager.progress_begin(0, nr_of_strips)
try:
self.report({'INFO'}, 'Rendering thumbnails for %i selected shots.' %
nr_of_strips)
for idx, strip in enumerate(context.selected_sequences):
strips = sorted(context.selected_sequences, key=self.by_frame)
for idx, strip in enumerate(strips):
context.window_manager.progress_update(idx)
# For multi-shot we can't just use the current frame (each thumb would be
# identical), so instead we use the middle frame. The first/last frames
# cannot be reliably used due to transitions with other shots.
self.set_middle_frame(context, strip)
# Pick the middle frame, except for the strip the original current frame
# marker was over.
if not self.strip_contains(strip, original_curframe):
self.set_middle_frame(context, strip)
else:
context.scene.frame_set(original_curframe)
await self.thumbnail_strip(context, strip)
if self._state == 'QUIT':
@@ -672,7 +751,7 @@ class ATTRACT_OT_make_shot_thumbnail(AttractOperatorMixin,
else:
strip = active_strip(context)
if not strip.frame_final_start <= context.scene.frame_current <= strip.frame_final_end:
if not self.strip_contains(strip, original_curframe):
self.report({'WARNING'}, 'Rendering middle frame as thumbnail for active shot.')
self.set_middle_frame(context, strip)
else:
@@ -692,12 +771,27 @@ class ATTRACT_OT_make_shot_thumbnail(AttractOperatorMixin,
self.report({'INFO'}, 'Thumbnail uploaded to Attract')
self.quit()
def set_middle_frame(self, context, strip):
@staticmethod
def strip_contains(strip, framenr: int) -> bool:
"""Returns True iff the strip covers the given frame number"""
return strip.frame_final_start <= framenr <= strip.frame_final_end
@staticmethod
def set_middle_frame(context, strip):
"""Sets the current frame to the middle frame of the strip."""
middle = round((strip.frame_final_start + strip.frame_final_end) / 2)
context.scene.frame_set(middle)
@staticmethod
def by_frame(sequence_strip) -> int:
"""Returns the start frame number of the sequence strip.
This can be used for sorting strips by time.
"""
return sequence_strip.frame_final_start
async def thumbnail_strip(self, context, strip):
atc_object_id = getattr(strip, 'atc_object_id', None)
if not atc_object_id:
@@ -773,6 +867,23 @@ class ATTRACT_OT_make_shot_thumbnail(AttractOperatorMixin,
return file_id
class ATTRACT_OT_copy_id_to_clipboard(AttractOperatorMixin, Operator):
bl_idname = 'attract.copy_id_to_clipboard'
bl_label = 'Copy shot ID to clipboard'
@classmethod
def poll(cls, context):
return bool(context.selected_sequences and active_strip(context))
def execute(self, context):
strip = active_strip(context)
context.window_manager.clipboard = strip.atc_object_id
self.report({'INFO'}, 'Shot ID %s copied to clipboard' % strip.atc_object_id)
return {'FINISHED'}
def draw_strip_movie_meta(self, context):
strip = active_strip(context)
if not strip:
@@ -829,11 +940,15 @@ def register():
bpy.utils.register_class(ATTRACT_OT_open_meta_blendfile)
bpy.utils.register_class(ATTRACT_OT_shot_open_in_browser)
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)
bpy.utils.unregister_module(__name__)
del bpy.types.Sequence.atc_is_synced
del bpy.types.Sequence.atc_object_id

View File

@@ -16,7 +16,7 @@
#
# ##### END GPL LICENSE BLOCK #####
# <pep8-80 compliant>
# <pep8 compliant>
import bpy
import logging
@@ -87,8 +87,7 @@ def draw_strip_conflict(strip_coords, pixel_size_x):
# Always draw the full rectangle, the conflict should be resolved and thus stand out.
bgl.glColor3f(*CONFLICT_COLOUR)
bgl.glLineWidth(3)
bgl.glPointSize(10)
bgl.glLineWidth(2)
bgl.glBegin(bgl.GL_LINE_LOOP)
bgl.glVertex2f(s_x1, s_y1)
@@ -97,13 +96,6 @@ def draw_strip_conflict(strip_coords, pixel_size_x):
bgl.glVertex2f(s_x1, s_y2)
bgl.glEnd()
bgl.glBegin(bgl.GL_POINTS)
bgl.glVertex2f(s_x1, s_y1)
bgl.glVertex2f(s_x2, s_y1)
bgl.glVertex2f(s_x2, s_y2)
bgl.glVertex2f(s_x1, s_y2)
bgl.glEnd()
bgl.glPopAttrib()

View File

@@ -285,12 +285,10 @@ class BlenderCloudPreferences(AddonPreferences):
# Image Share stuff
share_box = layout.box()
share_box.label('Image Sharing on Blender Cloud', icon_value=icon('CLOUD'))
share_box.enabled = msg_icon != 'ERROR'
share_box.prop(self, 'open_browser_after_share')
# Attract stuff
attract_box = layout.box()
attract_box.enabled = msg_icon != 'ERROR'
self.draw_attract_buttons(attract_box, self.attract_project)
def draw_subscribe_button(self, layout):

View File

@@ -553,6 +553,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
"""Draws the GUI with OpenGL."""
drawers = {
'INITIALIZING': self._draw_initializing,
'CHECKING_CREDENTIALS': self._draw_checking_credentials,
'BROWSING': self._draw_browser,
'DOWNLOADING_TEXTURE': self._draw_downloading,
@@ -653,6 +654,13 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
'Checking login credentials',
(0.0, 0.0, 0.2, 0.6))
def _draw_initializing(self, context):
"""OpenGL drawing code for the INITIALIZING state."""
self._draw_text_on_colour(context,
'Initializing',
(0.0, 0.0, 0.2, 0.6))
def _draw_text_on_colour(self, context, text, bgcolour):
content_height, content_width = self._window_size(context)
bgl.glEnable(bgl.GL_BLEND)

View File

@@ -18,11 +18,13 @@
# ##### END GPL LICENSE BLOCK #####
import glob
import os
import sys
import shutil
import subprocess
import re
import pathlib
import zipfile
from distutils import log
from distutils.core import Command
@@ -168,6 +170,34 @@ class BlenderAddonBdist(bdist):
super().run()
# noinspection PyAttributeOutsideInit
class BlenderAddonFdist(BlenderAddonBdist):
"""Ensures that 'python setup.py fdist' creates a plain folder structure."""
user_options = [
('dest-path=', None, 'addon installation path'),
]
def initialize_options(self):
super().initialize_options()
self.dest_path = None # path that will contain the addon
def run(self):
super().run()
# dist_files is a list of tuples ('bdist', 'any', 'filepath')
filepath = self.distribution.dist_files[0][2]
# if dest_path is not specified use the filename as the dest_path (minus the .zip)
assert filepath.endswith('.zip')
target_folder = self.dest_path or filepath[:-4]
print('Unzipping the package on {}.'.format(target_folder))
with zipfile.ZipFile(filepath, 'r') as zip_ref:
zip_ref.extractall(target_folder)
# noinspection PyAttributeOutsideInit
class BlenderAddonInstall(install):
"""Ensures the module is placed at the root of the zip file."""
@@ -191,12 +221,13 @@ class AvoidEggInfo(install_egg_info):
setup(
cmdclass={'bdist': BlenderAddonBdist,
'fdist': BlenderAddonFdist,
'install': BlenderAddonInstall,
'install_egg_info': AvoidEggInfo,
'wheels': BuildWheels},
name='blender_cloud',
description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.',
version='1.4.999',
version='1.5.2',
author='Sybren A. Stüvel',
author_email='sybren@stuvel.eu',
packages=find_packages('.'),

15
update_version.sh Executable file
View File

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