WIP: integration of the Attract addon into the Blender Cloud adddon.

This commit is contained in:
Sybren A. Stüvel 2016-08-01 14:40:56 +02:00
parent 63b976cb44
commit c57da7ab2b
4 changed files with 498 additions and 2 deletions

View File

@ -20,7 +20,7 @@
bl_info = { bl_info = {
'name': 'Blender Cloud', 'name': 'Blender Cloud',
'author': 'Sybren A. Stüvel and Francesco Siddi', "author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis",
'version': (1, 4, 3), 'version': (1, 4, 3),
'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',
@ -42,12 +42,13 @@ if 'pillar' in locals():
pillar = importlib.reload(pillar) pillar = importlib.reload(pillar)
cache = importlib.reload(cache) cache = importlib.reload(cache)
attract = importlib.reload(attract)
else: else:
from . import wheels from . import wheels
wheels.load_wheels() wheels.load_wheels()
from . import pillar, cache from . import pillar, cache, attract
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -89,6 +90,7 @@ def register():
blender.register() blender.register()
settings_sync.register() settings_sync.register()
image_sharing.register() image_sharing.register()
attract.register()
def _monkey_patch_requests(): def _monkey_patch_requests():
@ -112,6 +114,7 @@ def unregister():
from . import blender, texture_browser, async_loop, settings_sync, image_sharing from . import blender, texture_browser, async_loop, settings_sync, image_sharing
image_sharing.unregister() image_sharing.unregister()
attract.unregister()
settings_sync.unregister() settings_sync.unregister()
blender.unregister() blender.unregister()
texture_browser.unregister() texture_browser.unregister()

View File

@ -0,0 +1,339 @@
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
# Old info, kept here for reference, so that we can merge wiki pages,
# descriptions, etc.
#
# bl_info = {
# "name": "Attract",
# "author": "Francesco Siddi, Inês Almeida, Antony Riakiotakis",
# "version": (0, 2, 0),
# "blender": (2, 76, 0),
# "location": "Video Sequence Editor",
# "description":
# "Blender integration with the Attract task tracking service"
# ". *requires the Blender ID add-on",
# "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
# "Scripts/Workflow/Attract",
# "category": "Workflow",
# "support": "TESTING"
# }
if "bpy" in locals():
import importlib
importlib.reload(draw)
else:
from . import draw
import bpy
from pillarsdk.api import Api
from pillarsdk.nodes import Node
from pillarsdk.nodes import NodeType
from pillarsdk import utils
from pillarsdk.exceptions import ResourceNotFound
from bpy.props import StringProperty
from bpy.types import Operator, Panel, AddonPreferences
def active_strip(context):
try:
return context.scene.sequence_editor.active_strip
except AttributeError:
return None
def remove_atc_props(strip):
"""Resets the attract custom properties assigned to a VSE strip"""
strip.atc_cut_in = 0
strip.atc_cut_out = 0
strip.atc_name = ""
strip.atc_description = ""
strip.atc_object_id = ""
strip.atc_is_synced = False
class ToolsPanel(Panel):
bl_label = 'Attract'
bl_space_type = 'SEQUENCE_EDITOR'
bl_region_type = 'UI'
def draw_header(self, context):
strip = active_strip(context)
if strip and strip.atc_object_id:
self.layout.prop(strip, 'atc_is_synced', text='')
def draw(self, context):
strip = active_strip(context)
layout = self.layout
strip_types = {'MOVIE', 'IMAGE'}
if strip and strip.atc_object_id and strip.type in strip_types:
layout.prop(strip, 'atc_name', text='Name')
layout.prop(strip, 'atc_description', text='Description')
layout.prop(strip, 'atc_notes', text='Notes')
layout.prop(strip, 'atc_status', text='Status')
layout.prop(strip, 'atc_cut_in', text='Cut in')
# layout.prop(strip, 'atc_cut_out', text='Cut out')
if strip.atc_is_synced:
layout.operator('attract.shot_submit_update')
layout.operator('attract.shot_delete')
layout.operator('attract.strip_unlink')
elif strip and strip.type in strip_types:
layout.operator('attract.shot_submit_new')
layout.operator('attract.shot_relink')
else:
layout.label(text='Select a Movie or Image strip')
layout.operator('attract.shots_order_update')
class AttractShotSubmitNew(Operator):
bl_idname = "attract.shot_submit_new"
bl_label = "Submit to Attract"
@classmethod
def poll(cls, context):
strip = active_strip(context)
return not strip.atc_object_id
def execute(self, context):
from blender_cloud import pillar
import blender_id
strip = active_strip(context)
if strip.atc_object_id:
return
# Filter the NodeType collection, but it's still a list
node_type_list = pillar.call(NodeType.all, {'where': "name=='shot'"})
# Get the 'shot' node type
node_type = node_type_list['_items'][0]
# Define the shot properties
prop = {'name': strip.name,
'description': '',
'properties': {'status': 'on_hold',
'notes': '',
'cut_in': strip.frame_offset_start,
'cut_out': strip.frame_offset_start + strip.frame_final_duration},
'order': 0,
'node_type': node_type['_id'],
'user': blender_id.get_active_user_id()}
# Create a Node item with the attract API
node = Node(prop)
post = pillar.call(node.create)
# Populate the strip with the freshly generated ObjectID and info
if not post:
self.report({'ERROR'}, 'Error creating node! Check the console for now.')
return {'CANCELLED'}
strip.atc_object_id = node['_id']
strip.atc_is_synced = True
strip.atc_name = node['name']
strip.atc_cut_in = node['properties']['cut_in']
strip.atc_cut_out = node['properties']['cut_out']
return {'FINISHED'}
class AttractShotRelink(Operator):
bl_idname = "attract.shot_relink"
bl_label = "Relink to Attract"
strip_atc_object_id = bpy.props.StringProperty()
def execute(self, context):
from .. import pillar
strip = active_strip(context)
try:
node = pillar.call(Node.find, self.strip_atc_object_id)
except ResourceNotFound:
self.report({'ERROR'}, 'No shot found on the server')
return {'CANCELLED'}
strip.atc_object_id = self.strip_atc_object_id
strip.atc_is_synced = True
strip.atc_name = node.name
strip.atc_cut_in = node.properties.cut_in
strip.atc_cut_out = node.properties.cut_out
strip.atc_description = node.description
self.report({'INFO'}, "Shot {0} relinked".format(node.name))
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, 'strip_atc_object_id', text='Shot ID')
class AttractShotSubmitUpdate(Operator):
bl_idname = 'attract.shot_submit_update'
bl_label = 'Update'
bl_description = 'Syncronizes local and remote changes'
def execute(self, context):
strip = active_strip(context)
# Update cut_in and cut_out properties on the strip
# strip.atc_cut_in = strip.frame_offset_start
# strip.atc_cut_out = strip.frame_offset_start + strip.frame_final_duration
# print("Query Attract server with {0}".format(strip.atc_object_id))
strip.atc_cut_out = strip.atc_cut_in + strip.frame_final_duration - 1
node = Node.find(strip.atc_object_id)
node.name = strip.atc_name
node.description = strip.atc_description
node.properties.cut_in = strip.atc_cut_in
node.properties.cut_out = strip.atc_cut_out
node.update()
return {'FINISHED'}
class AttractShotDelete(Operator):
bl_idname = 'attract.shot_delete'
bl_label = 'Delete'
bl_description = 'Remove from Attract'
def execute(self, context):
strip = active_strip(context)
node = Node.find(strip.atc_object_id)
if node.delete():
remove_atc_props(strip)
return {'FINISHED'}
class AttractStripUnlink(Operator):
bl_idname = 'attract.strip_unlink'
bl_label = 'Unlink'
bl_description = 'Remove Attract props from the strip'
def execute(self, context):
strip = active_strip(context)
remove_atc_props(strip)
return {'FINISHED'}
class AttractShotsOrderUpdate(Operator):
bl_idname = 'attract.shots_order_update'
bl_label = 'Update shots order'
def execute(self, context):
from .. import pillar
# Get all shot nodes from server, build dictionary using ObjectID
# as indexes
node_type_list = pillar.call(NodeType.all, {'where': "name=='shot'"})
node_type = node_type_list._items[0]
shots = pillar.call(Node.all, {
'where': {'node_type': node_type._id},
'max_results': 100})
shots = shots._items
# TODO (fsiddi) take into account pagination. Currently we do not do it
# and it makes this dict useless.
# We should use the pagination info from the node_type_list query and
# keep querying until we have all the items.
shots_dict = {}
for shot in shots:
shots_dict[shot._id] = shot
# Build ordered list of strips from the edit.
strips_with_atc_object_id = [strip
for strip in context.scene.sequence_editor.sequences_all
if strip.atc_object_id]
strips_with_atc_object_id.sort(
key=lambda strip: strip.frame_start + strip.frame_offset_start)
index = 1
for strip in strips_with_atc_object_id:
"""
# Currently we use the code below to force update all nodes.
# Check that the shot is in the list of retrieved shots
if strip.atc_order != index: #or shots_dict[strip.atc_object_id]['order'] != index:
# If there is an update in the order, retrieve and update
# the node, as well as the VSE strip
# shot_node = Node.find(strip.atc_object_id)
# shot_node.order = index
# shot_node.update()
# strip.atc_order = index
print ("{0} > {1}".format(strip.atc_order, index))
"""
# We get all nodes one by one. This is bad and stupid.
try:
shot_node = pillar.call(Node.find, strip.atc_object_id)
# if shot_node.properties.order != index:
shot_node.order = index
shot_node.update()
print('{0} - updating {1}'.format(shot_node.order, shot_node.name))
strip.atc_order = index
index += 1
except ResourceNotFound:
# Reset the attract properties for any shot not found on the server
# print("Error: shot {0} not found".format(strip.atc_object_id))
remove_atc_props(strip)
return {'FINISHED'}
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")
bpy.types.Sequence.atc_name = bpy.props.StringProperty(name="Shot Name")
bpy.types.Sequence.atc_description = bpy.props.StringProperty(name="Shot description")
bpy.types.Sequence.atc_notes = bpy.props.StringProperty(name="Shot notes")
bpy.types.Sequence.atc_cut_in = bpy.props.IntProperty(name="Cut in")
bpy.types.Sequence.atc_cut_out = bpy.props.IntProperty(name="Cut out")
bpy.types.Sequence.atc_status = bpy.props.EnumProperty(
items=[
('on_hold', 'On hold', 'The shot is on hold'),
('todo', 'Todo', 'Waiting'),
('in_progress', 'In progress', 'The show has been assigned')],
name="Status")
bpy.types.Sequence.atc_order = bpy.props.IntProperty(name="Order")
bpy.utils.register_class(ToolsPanel)
bpy.utils.register_class(AttractShotSubmitNew)
bpy.utils.register_class(AttractShotRelink)
bpy.utils.register_class(AttractShotSubmitUpdate)
bpy.utils.register_class(AttractShotDelete)
bpy.utils.register_class(AttractStripUnlink)
bpy.utils.register_class(AttractShotsOrderUpdate)
draw.callback_enable()
def unregister():
draw.callback_disable()
del bpy.types.Sequence.atc_is_synced
del bpy.types.Sequence.atc_object_id
del bpy.types.Sequence.atc_name
del bpy.types.Sequence.atc_description
del bpy.types.Sequence.atc_notes
del bpy.types.Sequence.atc_cut_in
del bpy.types.Sequence.atc_cut_out
del bpy.types.Sequence.atc_status
del bpy.types.Sequence.atc_order
bpy.utils.unregister_module(__name__)

View File

@ -0,0 +1,142 @@
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8-80 compliant>
import bpy
def get_strip_rectf(strip):
# Get x and y in terms of the grid's frames and channels
x1 = strip.frame_final_start
x2 = strip.frame_final_end
y1 = strip.channel + 0.2
y2 = y1 + 0.25
return [x1, y1, x2, y2]
def draw_underline_in_strip(scroller_width, strip_coords, curx, color):
from bgl import glColor4f, glRectf, glEnable, glDisable, GL_BLEND
context = bpy.context
# Strip coords
s_x1, s_y1, s_x2, s_y2 = strip_coords
# Drawing coords
x = 0
d_y1 = s_y1
d_y2 = s_y2
d_x1 = s_x1
d_x2 = s_x2
# be careful not to override the current frame line
cf_x = context.scene.frame_current_final
y = 0
r, g, b, a = color
glColor4f(r, g, b, a)
glEnable(GL_BLEND)
# // this checks if the strip range overlaps the current f. label range
# // then it would need a polygon? to draw around it
# // TODO: check also if label display is ON
# Check if the current frame label overlaps the strip
# label_height = scroller_width * 2
# if d_y1 < label_height:
# if cf_x < d_x2 and d_x1 < cf_x + label_height:
# print("ALARM!!")
if d_x1 < cf_x and cf_x < d_x2:
# Bad luck, the line passes our strip
glRectf(d_x1, d_y1, cf_x - curx, d_y2)
glRectf(cf_x + curx, d_y1, d_x2, d_y2)
else:
# Normal, full rectangle draw
glRectf(d_x1, d_y1, d_x2, d_y2)
glDisable(GL_BLEND)
def draw_callback_px():
context = bpy.context
if not context.scene.sequence_editor:
return
# Calculate scroller width, dpi and pixelsize dependent
pixel_size = context.user_preferences.system.pixel_size
dpi = context.user_preferences.system.dpi
dpi_fac = pixel_size * dpi / 72
# A normal widget unit is 20, but the scroller is apparently 16
scroller_width = 16 * dpi_fac
region = context.region
xwin1, ywin1 = region.view2d.region_to_view(0, 0)
xwin2, ywin2 = region.view2d.region_to_view(region.width, region.height)
curx, cury = region.view2d.region_to_view(1, 0)
curx = curx - xwin1
for strip in context.scene.sequence_editor.sequences:
if strip.atc_object_id:
# Get corners (x1, y1), (x2, y2) of the strip rectangle in px region coords
strip_coords = get_strip_rectf(strip)
#check if any of the coordinates are out of bounds
if strip_coords[0] > xwin2 or strip_coords[2] < xwin1 or strip_coords[1] > ywin2 or strip_coords[3] < ywin1:
continue
# Draw
color = [1.0, 0, 1.0, 0.5]
draw_underline_in_strip(scroller_width, strip_coords, curx, color)
def tag_redraw_all_sequencer_editors():
context = bpy.context
# Py cant access notifiers
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'SEQUENCE_EDITOR':
for region in area.regions:
if region.type == 'WINDOW':
region.tag_redraw()
# This is a list so it can be changed instead of set
# if it is only changed, it does not have to be declared as a global everywhere
cb_handle = []
def callback_enable():
if cb_handle:
return
cb_handle[:] = bpy.types.SpaceSequenceEditor.draw_handler_add(
draw_callback_px, (), 'WINDOW', 'POST_VIEW'),
tag_redraw_all_sequencer_editors()
def callback_disable():
if not cb_handle:
return
bpy.types.SpaceSequenceEditor.draw_handler_remove(cb_handle[0], 'WINDOW')
tag_redraw_all_sequencer_editors()

View File

@ -198,6 +198,12 @@ pillar_semaphore = asyncio.Semaphore(3)
async def pillar_call(pillar_func, *args, caching=True, **kwargs): async def pillar_call(pillar_func, *args, caching=True, **kwargs):
"""Calls a Pillar function.
A semaphore is used to ensure that there won't be too many
calls to Pillar simultaneously.
"""
partial = functools.partial(pillar_func, *args, api=pillar_api(caching=caching), **kwargs) partial = functools.partial(pillar_func, *args, api=pillar_api(caching=caching), **kwargs)
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
@ -205,6 +211,12 @@ async def pillar_call(pillar_func, *args, caching=True, **kwargs):
return await loop.run_in_executor(None, partial) return await loop.run_in_executor(None, partial)
def call(pillar_func, *args, **kwargs):
"""Synchronous call to Pillar, ensures the correct Api object is used."""
return pillar_func(*args, api=pillar_api(), **kwargs)
async def check_pillar_credentials(required_roles: set): async def check_pillar_credentials(required_roles: set):
"""Tries to obtain the user at Pillar using the user's credentials. """Tries to obtain the user at Pillar using the user's credentials.