WIP: integration of the Attract addon into the Blender Cloud adddon.
This commit is contained in:
parent
63b976cb44
commit
c57da7ab2b
@ -20,7 +20,7 @@
|
||||
|
||||
bl_info = {
|
||||
'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),
|
||||
'blender': (2, 77, 0),
|
||||
'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)
|
||||
cache = importlib.reload(cache)
|
||||
attract = importlib.reload(attract)
|
||||
else:
|
||||
from . import wheels
|
||||
|
||||
wheels.load_wheels()
|
||||
|
||||
from . import pillar, cache
|
||||
from . import pillar, cache, attract
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -89,6 +90,7 @@ def register():
|
||||
blender.register()
|
||||
settings_sync.register()
|
||||
image_sharing.register()
|
||||
attract.register()
|
||||
|
||||
|
||||
def _monkey_patch_requests():
|
||||
@ -112,6 +114,7 @@ def unregister():
|
||||
from . import blender, texture_browser, async_loop, settings_sync, image_sharing
|
||||
|
||||
image_sharing.unregister()
|
||||
attract.unregister()
|
||||
settings_sync.unregister()
|
||||
blender.unregister()
|
||||
texture_browser.unregister()
|
||||
|
339
blender_cloud/attract/__init__.py
Normal file
339
blender_cloud/attract/__init__.py
Normal 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__)
|
142
blender_cloud/attract/draw.py
Normal file
142
blender_cloud/attract/draw.py
Normal 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()
|
@ -198,6 +198,12 @@ pillar_semaphore = asyncio.Semaphore(3)
|
||||
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
"""Tries to obtain the user at Pillar using the user's credentials.
|
||||
|
||||
|
Reference in New Issue
Block a user