Compare commits
45 Commits
version-1.
...
version-1.
Author | SHA1 | Date | |
---|---|---|---|
68d2fc8e42 | |||
2de4a8e87c | |||
39b2bacdcc | |||
a1416f99dd | |||
0fa7d60028 | |||
c1b6480f9a | |||
56353d4177 | |||
469a9318af | |||
e265081131 | |||
115eea82c6 | |||
900068a6f5 | |||
c8229500d1 | |||
65ff9da428 | |||
fcba8a2e0f | |||
7ef5e522f8 | |||
ae570e5907 | |||
16b90d2ea8 | |||
875c92ee9d | |||
cbfc75a89c | |||
54e676b36f | |||
c94b0f5f2d | |||
a58bfe9a76 | |||
d332e1e50a | |||
![]() |
dd66d5ce93 | ||
965b02cec4 | |||
fd67675c12 | |||
06a661126b | |||
191b150280 | |||
feb62ddae0 | |||
603159f0d1 | |||
d2ae3f9cb7 | |||
079f8ff4c3 | |||
c97859ef33 | |||
f1ebea8948 | |||
1b82977c6e | |||
a7307bf7b5 | |||
11fd12e125 | |||
54dccb20ba | |||
61a8db3f96 | |||
0b2f0a3ec1 | |||
5117ec7cde | |||
74f61fa83a | |||
f73671c4f0 | |||
6f970a41e5 | |||
ccedb7cbb1 |
@@ -21,15 +21,14 @@
|
|||||||
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, 4, 99),
|
'version': (1, 5, 0),
|
||||||
'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 '
|
||||||
'and Blender 2.77a or newer.',
|
'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',
|
'Scripts/System/BlenderCloud',
|
||||||
'category': 'System',
|
'category': 'System',
|
||||||
'warning': 'This is a beta version; the first to support Attract.'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
@@ -36,17 +36,22 @@
|
|||||||
# "support": "TESTING"
|
# "support": "TESTING"
|
||||||
# }
|
# }
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
if "bpy" in locals():
|
if "bpy" in locals():
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
importlib.reload(draw)
|
draw = importlib.reload(draw)
|
||||||
|
pillar = importlib.reload(pillar)
|
||||||
|
async_loop = importlib.reload(async_loop)
|
||||||
else:
|
else:
|
||||||
from . import draw
|
from . import draw
|
||||||
|
from .. import pillar, async_loop
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
import pillarsdk
|
||||||
from pillarsdk.nodes import Node
|
from pillarsdk.nodes import Node
|
||||||
from pillarsdk.projects import Project
|
from pillarsdk.projects import Project
|
||||||
from pillarsdk import exceptions as sdk_exceptions
|
from pillarsdk import exceptions as sdk_exceptions
|
||||||
@@ -63,6 +68,40 @@ def active_strip(context):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def selected_shots(context):
|
||||||
|
"""Generator, yields selected strips if they are Attract shots."""
|
||||||
|
|
||||||
|
for strip in context.selected_sequences:
|
||||||
|
atc_object_id = getattr(strip, 'atc_object_id')
|
||||||
|
if not atc_object_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
yield strip
|
||||||
|
|
||||||
|
|
||||||
|
def all_shots(context):
|
||||||
|
"""Generator, yields all strips if they are Attract shots."""
|
||||||
|
|
||||||
|
for strip in context.scene.sequence_editor.sequences_all:
|
||||||
|
atc_object_id = getattr(strip, 'atc_object_id')
|
||||||
|
if not atc_object_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
yield strip
|
||||||
|
|
||||||
|
|
||||||
|
def shown_strips(context):
|
||||||
|
"""Returns the strips from the current meta-strip-stack, or top-level strips.
|
||||||
|
|
||||||
|
What is returned depends on what the user is currently editing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if context.scene.sequence_editor.meta_stack:
|
||||||
|
return context.scene.sequence_editor.meta_stack[-1].sequences
|
||||||
|
|
||||||
|
return context.scene.sequence_editor.sequences
|
||||||
|
|
||||||
|
|
||||||
def remove_atc_props(strip):
|
def remove_atc_props(strip):
|
||||||
"""Resets the attract custom properties assigned to a VSE strip"""
|
"""Resets the attract custom properties assigned to a VSE strip"""
|
||||||
|
|
||||||
@@ -72,6 +111,47 @@ def remove_atc_props(strip):
|
|||||||
strip.atc_is_synced = False
|
strip.atc_is_synced = False
|
||||||
|
|
||||||
|
|
||||||
|
def shot_id_use(strips):
|
||||||
|
"""Returns a mapping from shot Object ID to a list of strips that use it."""
|
||||||
|
|
||||||
|
import collections
|
||||||
|
|
||||||
|
# Count the number of uses per Object ID, so that we can highlight double use.
|
||||||
|
ids_in_use = collections.defaultdict(list)
|
||||||
|
for strip in strips:
|
||||||
|
if not getattr(strip, 'atc_is_synced', False):
|
||||||
|
continue
|
||||||
|
|
||||||
|
ids_in_use[strip.atc_object_id].append(strip)
|
||||||
|
|
||||||
|
return ids_in_use
|
||||||
|
|
||||||
|
|
||||||
|
def compute_strip_conflicts(scene):
|
||||||
|
"""Sets the strip property atc_object_id_conflict for each strip."""
|
||||||
|
|
||||||
|
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):
|
class ToolsPanel(Panel):
|
||||||
bl_label = 'Attract'
|
bl_label = 'Attract'
|
||||||
bl_space_type = 'SEQUENCE_EDITOR'
|
bl_space_type = 'SEQUENCE_EDITOR'
|
||||||
@@ -85,8 +165,21 @@ class ToolsPanel(Panel):
|
|||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
strip = active_strip(context)
|
strip = active_strip(context)
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
strip_types = {'MOVIE', 'IMAGE'}
|
strip_types = {'MOVIE', 'IMAGE', 'META'}
|
||||||
if strip and strip.atc_object_id and strip.type in strip_types:
|
|
||||||
|
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'
|
||||||
|
else:
|
||||||
|
noun = 'This Shot'
|
||||||
|
|
||||||
|
if strip.atc_object_id_conflict:
|
||||||
|
warnbox = layout.box()
|
||||||
|
warnbox.alert = True
|
||||||
|
warnbox.label('Warning: This shot is linked to multiple sequencer strips.',
|
||||||
|
icon='ERROR')
|
||||||
|
|
||||||
layout.prop(strip, 'atc_name', text='Name')
|
layout.prop(strip, 'atc_name', text='Name')
|
||||||
layout.prop(strip, 'atc_status', text='Status')
|
layout.prop(strip, 'atc_status', text='Status')
|
||||||
|
|
||||||
@@ -97,23 +190,34 @@ class ToolsPanel(Panel):
|
|||||||
ro_sub.prop(strip, 'atc_notes', text='Notes')
|
ro_sub.prop(strip, 'atc_notes', text='Notes')
|
||||||
|
|
||||||
if strip.atc_is_synced:
|
if strip.atc_is_synced:
|
||||||
row = layout.row(align=True)
|
sub = layout.column(align=True)
|
||||||
row.operator('attract.shot_submit_update')
|
row = sub.row(align=True)
|
||||||
|
if bpy.ops.attract.submit_selected.poll():
|
||||||
|
row.operator('attract.submit_selected', text='Submit %s' % noun)
|
||||||
|
else:
|
||||||
|
row.operator(ATTRACT_OT_submit_all.bl_idname)
|
||||||
row.operator(AttractShotFetchUpdate.bl_idname,
|
row.operator(AttractShotFetchUpdate.bl_idname,
|
||||||
text='', icon='FILE_REFRESH')
|
text='', icon='FILE_REFRESH')
|
||||||
|
row.operator(ATTRACT_OT_shot_open_in_browser.bl_idname,
|
||||||
|
text='', icon='WORLD')
|
||||||
|
sub.operator(ATTRACT_OT_make_shot_thumbnail.bl_idname,
|
||||||
|
text='Render Thumbnail for %s' % noun)
|
||||||
|
|
||||||
# Group more dangerous operations.
|
# Group more dangerous operations.
|
||||||
dangerous_sub = layout.column(align=True)
|
dangerous_sub = layout.column(align=True)
|
||||||
dangerous_sub.operator('attract.shot_delete')
|
dangerous_sub.operator(AttractShotDelete.bl_idname)
|
||||||
dangerous_sub.operator('attract.strip_unlink')
|
dangerous_sub.operator('attract.strip_unlink')
|
||||||
|
|
||||||
elif strip and strip.type in strip_types:
|
elif context.selected_sequences:
|
||||||
layout.operator('attract.shot_submit_new')
|
if len(context.selected_sequences) > 1:
|
||||||
|
noun = 'Selected Strips'
|
||||||
|
else:
|
||||||
|
noun = 'This Strip'
|
||||||
|
layout.operator(AttractShotSubmitSelected.bl_idname,
|
||||||
|
text='Submit %s as New Shot' % noun)
|
||||||
layout.operator('attract.shot_relink')
|
layout.operator('attract.shot_relink')
|
||||||
else:
|
else:
|
||||||
layout.label(text='Select a Movie or Image strip')
|
layout.operator(ATTRACT_OT_submit_all.bl_idname)
|
||||||
|
|
||||||
layout.operator(AttractShotSubmitSelected.bl_idname)
|
|
||||||
|
|
||||||
|
|
||||||
class AttractOperatorMixin:
|
class AttractOperatorMixin:
|
||||||
@@ -166,6 +270,7 @@ class AttractOperatorMixin:
|
|||||||
'description': '',
|
'description': '',
|
||||||
'properties': {'status': 'todo',
|
'properties': {'status': 'todo',
|
||||||
'notes': '',
|
'notes': '',
|
||||||
|
'used_in_edit': True,
|
||||||
'trim_start_in_frames': strip.frame_offset_start,
|
'trim_start_in_frames': strip.frame_offset_start,
|
||||||
'duration_in_edit_in_frames': strip.frame_final_duration,
|
'duration_in_edit_in_frames': strip.frame_final_duration,
|
||||||
'cut_in_timeline_in_frames': strip.frame_final_start},
|
'cut_in_timeline_in_frames': strip.frame_final_start},
|
||||||
@@ -204,6 +309,7 @@ class AttractOperatorMixin:
|
|||||||
'properties.duration_in_edit_in_frames': strip.frame_final_duration,
|
'properties.duration_in_edit_in_frames': strip.frame_final_duration,
|
||||||
'properties.cut_in_timeline_in_frames': strip.frame_final_start,
|
'properties.cut_in_timeline_in_frames': strip.frame_final_start,
|
||||||
'properties.status': strip.atc_status,
|
'properties.status': strip.atc_status,
|
||||||
|
'properties.used_in_edit': True,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,17 +317,25 @@ class AttractOperatorMixin:
|
|||||||
result = pillar.sync_call(node.patch, patch)
|
result = pillar.sync_call(node.patch, patch)
|
||||||
log.info('PATCH result: %s', result)
|
log.info('PATCH result: %s', result)
|
||||||
|
|
||||||
def relink(self, strip, atc_object_id):
|
def relink(self, strip, atc_object_id, *, refresh=False):
|
||||||
from .. import pillar
|
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:
|
try:
|
||||||
node = pillar.sync_call(Node.find, atc_object_id)
|
node = pillar.sync_call(Node.find, atc_object_id, caching=False)
|
||||||
except (sdk_exceptions.ResourceNotFound, sdk_exceptions.MethodNotAllowed):
|
except (sdk_exceptions.ResourceNotFound, sdk_exceptions.MethodNotAllowed):
|
||||||
self.report({'ERROR'}, 'Shot %r not found on the Attract server, unable to relink.'
|
verb = 'refresh' if refresh else 'relink'
|
||||||
% atc_object_id)
|
self.report({'ERROR'}, 'Shot %r not found on the Attract server, unable to %s.'
|
||||||
|
% (atc_object_id, verb))
|
||||||
|
strip.atc_is_synced = False
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
strip.atc_is_synced = True
|
strip.atc_is_synced = True
|
||||||
|
if not refresh:
|
||||||
strip.atc_name = node.name
|
strip.atc_name = node.name
|
||||||
strip.atc_object_id = node['_id']
|
strip.atc_object_id = node['_id']
|
||||||
|
|
||||||
@@ -229,54 +343,31 @@ class AttractOperatorMixin:
|
|||||||
strip.atc_status = node.properties.status
|
strip.atc_status = node.properties.status
|
||||||
strip.atc_notes = node.properties.notes or ''
|
strip.atc_notes = node.properties.notes or ''
|
||||||
strip.atc_description = node.description or ''
|
strip.atc_description = node.description or ''
|
||||||
|
|
||||||
draw.tag_redraw_all_sequencer_editors()
|
draw.tag_redraw_all_sequencer_editors()
|
||||||
|
|
||||||
|
|
||||||
class AttractShotSubmitNew(AttractOperatorMixin, 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):
|
|
||||||
strip = active_strip(context)
|
|
||||||
if strip.atc_object_id:
|
|
||||||
return
|
|
||||||
|
|
||||||
node_type = self.find_node_type('attract_shot')
|
|
||||||
if isinstance(node_type, set): # in case of error
|
|
||||||
return node_type
|
|
||||||
|
|
||||||
return self.submit_new_strip(strip) or {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class AttractShotFetchUpdate(AttractOperatorMixin, Operator):
|
class AttractShotFetchUpdate(AttractOperatorMixin, Operator):
|
||||||
bl_idname = "attract.shot_fetch_update"
|
bl_idname = "attract.shot_fetch_update"
|
||||||
bl_label = "Fetch update from Attract"
|
bl_label = "Fetch Update From Attract"
|
||||||
|
bl_description = 'Update status, description & notes from Attract'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
strip = active_strip(context)
|
return any(selected_shots(context))
|
||||||
return strip is not None and getattr(strip, 'atc_object_id', None)
|
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
strip = active_strip(context)
|
for strip in selected_shots(context):
|
||||||
|
status = self.relink(strip, strip.atc_object_id, refresh=True)
|
||||||
status = self.relink(strip, strip.atc_object_id)
|
# We don't abort when one strip fails. All selected shots should be
|
||||||
if isinstance(status, set):
|
# refreshed, even if one can't be found (for example).
|
||||||
return status
|
if not isinstance(status, set):
|
||||||
|
|
||||||
self.report({'INFO'}, "Shot {0} refreshed".format(strip.atc_name))
|
self.report({'INFO'}, "Shot {0} refreshed".format(strip.atc_name))
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class AttractShotRelink(AttractShotFetchUpdate):
|
class AttractShotRelink(AttractShotFetchUpdate):
|
||||||
bl_idname = "attract.shot_relink"
|
bl_idname = "attract.shot_relink"
|
||||||
bl_label = "Relink with Attract"
|
bl_label = "Relink With Attract"
|
||||||
|
|
||||||
strip_atc_object_id = bpy.props.StringProperty()
|
strip_atc_object_id = bpy.props.StringProperty()
|
||||||
|
|
||||||
@@ -298,6 +389,15 @@ class AttractShotRelink(AttractShotFetchUpdate):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
|
maybe_id = context.window_manager.clipboard
|
||||||
|
if len(maybe_id) == 24:
|
||||||
|
try:
|
||||||
|
int(maybe_id, 16)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.strip_atc_object_id = maybe_id
|
||||||
|
|
||||||
return context.window_manager.invoke_props_dialog(self)
|
return context.window_manager.invoke_props_dialog(self)
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
@@ -306,23 +406,30 @@ class AttractShotRelink(AttractShotFetchUpdate):
|
|||||||
col.prop(self, 'strip_atc_object_id', text='Shot ID')
|
col.prop(self, 'strip_atc_object_id', text='Shot ID')
|
||||||
|
|
||||||
|
|
||||||
class AttractShotSubmitUpdate(AttractOperatorMixin, Operator):
|
class ATTRACT_OT_shot_open_in_browser(AttractOperatorMixin, Operator):
|
||||||
bl_idname = 'attract.shot_submit_update'
|
bl_idname = 'attract.shot_open_in_browser'
|
||||||
bl_label = 'Submit update'
|
bl_label = 'Open in Browser'
|
||||||
bl_description = 'Sends local changes to Attract'
|
bl_description = 'Opens a webbrowser to show the shot on Attract'
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
strip = active_strip(context)
|
from ..blender import PILLAR_WEB_SERVER_URL
|
||||||
self.submit_update(strip)
|
import webbrowser
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
strip = active_strip(context)
|
||||||
|
|
||||||
|
url = urllib.parse.urljoin(PILLAR_WEB_SERVER_URL,
|
||||||
|
'nodes/%s/redir' % strip.atc_object_id)
|
||||||
|
webbrowser.open_new_tab(url)
|
||||||
|
self.report({'INFO'}, 'Opened a browser at %s' % url)
|
||||||
|
|
||||||
self.report({'INFO'}, 'Shot was updated on Attract')
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class AttractShotDelete(AttractOperatorMixin, Operator):
|
class AttractShotDelete(AttractOperatorMixin, Operator):
|
||||||
bl_idname = 'attract.shot_delete'
|
bl_idname = 'attract.shot_delete'
|
||||||
bl_label = 'Delete'
|
bl_label = 'Delete Shot'
|
||||||
bl_description = 'Remove from Attract'
|
bl_description = 'Remove this shot from Attract'
|
||||||
|
|
||||||
confirm = bpy.props.BoolProperty(name='confirm')
|
confirm = bpy.props.BoolProperty(name='confirm')
|
||||||
|
|
||||||
@@ -344,26 +451,43 @@ class AttractShotDelete(AttractOperatorMixin, Operator):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
|
self.confirm = False
|
||||||
return context.window_manager.invoke_props_dialog(self)
|
return context.window_manager.invoke_props_dialog(self)
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
col = layout.column()
|
col = layout.column()
|
||||||
col.prop(self, 'confirm', text="I hereby confirm I want to delete this shot.")
|
col.prop(self, 'confirm', text="I hereby confirm: delete this shot from The Edit.")
|
||||||
|
|
||||||
|
|
||||||
class AttractStripUnlink(AttractOperatorMixin, Operator):
|
class AttractStripUnlink(AttractOperatorMixin, Operator):
|
||||||
bl_idname = 'attract.strip_unlink'
|
bl_idname = 'attract.strip_unlink'
|
||||||
bl_label = 'Unlink'
|
bl_label = 'Unlink Shot From This Strip'
|
||||||
bl_description = 'Remove Attract props from the selected strip(s)'
|
bl_description = 'Remove Attract props from the selected strip(s)'
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
|
unlinked_ids = set()
|
||||||
|
|
||||||
|
# First remove the Attract properties from the strips.
|
||||||
for strip in context.selected_sequences:
|
for strip in context.selected_sequences:
|
||||||
atc_object_id = getattr(strip, 'atc_object_id')
|
atc_object_id = getattr(strip, 'atc_object_id')
|
||||||
remove_atc_props(strip)
|
remove_atc_props(strip)
|
||||||
|
|
||||||
if atc_object_id:
|
if atc_object_id:
|
||||||
self.report({'INFO'}, 'Shot %s has been unlinked from Attract.' % atc_object_id)
|
unlinked_ids.add(atc_object_id)
|
||||||
|
|
||||||
|
# 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.scene)
|
||||||
|
for oid in unlinked_ids:
|
||||||
|
if len(id_to_shots[oid]):
|
||||||
|
# Still in use
|
||||||
|
continue
|
||||||
|
|
||||||
|
node = Node({'_id': oid})
|
||||||
|
pillar.sync_call(node.patch, {'op': 'unlink'})
|
||||||
|
self.report({'INFO'}, 'Shot %s has been marked as Unused.' % oid)
|
||||||
|
|
||||||
draw.tag_redraw_all_sequencer_editors()
|
draw.tag_redraw_all_sequencer_editors()
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
@@ -371,7 +495,7 @@ class AttractStripUnlink(AttractOperatorMixin, Operator):
|
|||||||
|
|
||||||
class AttractShotSubmitSelected(AttractOperatorMixin, Operator):
|
class AttractShotSubmitSelected(AttractOperatorMixin, Operator):
|
||||||
bl_idname = 'attract.submit_selected'
|
bl_idname = 'attract.submit_selected'
|
||||||
bl_label = 'Submit all selected'
|
bl_label = 'Submit All Selected'
|
||||||
bl_description = 'Submits all selected strips to Attract'
|
bl_description = 'Submits all selected strips to Attract'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -380,9 +504,9 @@ class AttractShotSubmitSelected(AttractOperatorMixin, Operator):
|
|||||||
|
|
||||||
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.
|
||||||
node_type = self.find_node_type('attract_shot')
|
maybe_error = self.find_node_type('attract_shot')
|
||||||
if isinstance(node_type, set):
|
if isinstance(maybe_error, set):
|
||||||
return node_type
|
return maybe_error
|
||||||
|
|
||||||
for strip in context.selected_sequences:
|
for strip in context.selected_sequences:
|
||||||
status = self.submit(strip)
|
status = self.submit(strip)
|
||||||
@@ -404,43 +528,361 @@ class AttractShotSubmitSelected(AttractOperatorMixin, Operator):
|
|||||||
return self.submit_update(strip)
|
return self.submit_update(strip)
|
||||||
|
|
||||||
|
|
||||||
|
class ATTRACT_OT_submit_all(AttractOperatorMixin, Operator):
|
||||||
|
bl_idname = 'attract.submit_all'
|
||||||
|
bl_label = 'Submit All Shots to Attract'
|
||||||
|
bl_description = 'Updates Attract with the current state of the edit'
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
# Check that the project is set up for Attract.
|
||||||
|
maybe_error = self.find_node_type('attract_shot')
|
||||||
|
if isinstance(maybe_error, set):
|
||||||
|
return maybe_error
|
||||||
|
|
||||||
|
for strip in all_shots(context):
|
||||||
|
status = self.submit_update(strip)
|
||||||
|
if isinstance(status, set):
|
||||||
|
return status
|
||||||
|
|
||||||
|
self.report({'INFO'}, 'All strips re-sent to Attract.')
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class ATTRACT_OT_open_meta_blendfile(AttractOperatorMixin, Operator):
|
||||||
|
bl_idname = 'attract.open_meta_blendfile'
|
||||||
|
bl_label = 'Open Blendfile'
|
||||||
|
bl_description = 'Open Blendfile from movie strip metadata'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return bool(any(cls.filename_from_metadata(s) for s in context.selected_sequences))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def filename_from_metadata(strip):
|
||||||
|
"""Returns the blendfile name from the strip metadata, or None."""
|
||||||
|
|
||||||
|
# Metadata is a dict like:
|
||||||
|
# meta = {'END_FRAME': '88',
|
||||||
|
# 'BLEND_FILE': 'metadata-test.blend',
|
||||||
|
# 'SCENE': 'SüperSčene',
|
||||||
|
# 'FRAME_STEP': '1',
|
||||||
|
# 'START_FRAME': '32'}
|
||||||
|
|
||||||
|
meta = strip.get('metadata', None)
|
||||||
|
if not meta:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return meta.get('BLEND_FILE', None) or None
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
for strip in context.selected_sequences:
|
||||||
|
meta = strip.get('metadata', None)
|
||||||
|
if not meta:
|
||||||
|
continue
|
||||||
|
|
||||||
|
fname = meta.get('BLEND_FILE', None)
|
||||||
|
if not fname: continue
|
||||||
|
|
||||||
|
scene = meta.get('SCENE', None)
|
||||||
|
self.open_in_new_blender(fname, scene)
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def open_in_new_blender(self, fname, scene):
|
||||||
|
"""
|
||||||
|
:type fname: str
|
||||||
|
:type scene: str
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
bpy.app.binary_path,
|
||||||
|
str(fname),
|
||||||
|
]
|
||||||
|
|
||||||
|
cmd[1:1] = [v for v in sys.argv if v.startswith('--enable-')]
|
||||||
|
|
||||||
|
if scene:
|
||||||
|
cmd.extend(['--python-expr',
|
||||||
|
'import bpy; bpy.context.screen.scene = bpy.data.scenes["%s"]' % scene])
|
||||||
|
cmd.extend(['--scene', scene])
|
||||||
|
|
||||||
|
subprocess.Popen(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
class ATTRACT_OT_make_shot_thumbnail(AttractOperatorMixin,
|
||||||
|
async_loop.AsyncModalOperatorMixin,
|
||||||
|
Operator):
|
||||||
|
bl_idname = 'attract.make_shot_thumbnail'
|
||||||
|
bl_label = 'Render Shot Thumbnail'
|
||||||
|
bl_description = 'Renders the current frame, and uploads it as thumbnail for the shot'
|
||||||
|
|
||||||
|
stop_upon_exception = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return bool(context.selected_sequences)
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def thumbnail_render_settings(self, context, thumbnail_width=512):
|
||||||
|
# Remember current settings so we can restore them later.
|
||||||
|
orig_res_x = context.scene.render.resolution_x
|
||||||
|
orig_res_y = context.scene.render.resolution_y
|
||||||
|
orig_percentage = context.scene.render.resolution_percentage
|
||||||
|
orig_file_format = context.scene.render.image_settings.file_format
|
||||||
|
orig_quality = context.scene.render.image_settings.quality
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Update the render size to something thumbnaily.
|
||||||
|
factor = orig_res_y / orig_res_x
|
||||||
|
context.scene.render.resolution_x = thumbnail_width
|
||||||
|
context.scene.render.resolution_y = round(thumbnail_width * factor)
|
||||||
|
context.scene.render.resolution_percentage = 100
|
||||||
|
context.scene.render.image_settings.file_format = 'JPEG'
|
||||||
|
context.scene.render.image_settings.quality = 85
|
||||||
|
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
# Return the render settings to normal.
|
||||||
|
context.scene.render.resolution_x = orig_res_x
|
||||||
|
context.scene.render.resolution_y = orig_res_y
|
||||||
|
context.scene.render.resolution_percentage = orig_percentage
|
||||||
|
context.scene.render.image_settings.file_format = orig_file_format
|
||||||
|
context.scene.render.image_settings.quality = orig_quality
|
||||||
|
|
||||||
|
@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 current_frame
|
||||||
|
finally:
|
||||||
|
context.scene.frame_current = current_frame
|
||||||
|
|
||||||
|
async def async_execute(self, context):
|
||||||
|
nr_of_strips = len(context.selected_sequences)
|
||||||
|
do_multishot = nr_of_strips > 1
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
strips = sorted(context.selected_sequences, key=self.by_frame)
|
||||||
|
for idx, strip in enumerate(strips):
|
||||||
|
context.window_manager.progress_update(idx)
|
||||||
|
|
||||||
|
# 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':
|
||||||
|
return
|
||||||
|
context.window_manager.progress_update(nr_of_strips)
|
||||||
|
finally:
|
||||||
|
context.window_manager.progress_end()
|
||||||
|
|
||||||
|
else:
|
||||||
|
strip = active_strip(context)
|
||||||
|
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:
|
||||||
|
self.report({'INFO'}, 'Rendering current frame as thumbnail for active shot.')
|
||||||
|
|
||||||
|
context.window_manager.progress_begin(0, 1)
|
||||||
|
context.window_manager.progress_update(0)
|
||||||
|
try:
|
||||||
|
await self.thumbnail_strip(context, strip)
|
||||||
|
finally:
|
||||||
|
context.window_manager.progress_update(1)
|
||||||
|
context.window_manager.progress_end()
|
||||||
|
|
||||||
|
if self._state == 'QUIT':
|
||||||
|
return
|
||||||
|
|
||||||
|
self.report({'INFO'}, 'Thumbnail uploaded to Attract')
|
||||||
|
self.quit()
|
||||||
|
|
||||||
|
@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:
|
||||||
|
self.report({'ERROR'}, 'Strip %s not set up for Attract' % strip.name)
|
||||||
|
self.quit()
|
||||||
|
return
|
||||||
|
|
||||||
|
with self.thumbnail_render_settings(context):
|
||||||
|
bpy.ops.render.render()
|
||||||
|
file_id = await self.upload_via_tempdir(bpy.data.images['Render Result'],
|
||||||
|
'attract_shot_thumbnail.jpg')
|
||||||
|
|
||||||
|
if file_id is None:
|
||||||
|
self.quit()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update the shot to include this file as the picture.
|
||||||
|
node = pillarsdk.Node({'_id': atc_object_id})
|
||||||
|
await pillar.pillar_call(
|
||||||
|
node.patch,
|
||||||
|
{
|
||||||
|
'op': 'from-blender',
|
||||||
|
'$set': {
|
||||||
|
'picture': file_id,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async def upload_via_tempdir(self, datablock, filename_on_cloud) -> pillarsdk.Node:
|
||||||
|
"""Saves the datablock to file, and uploads it to the cloud.
|
||||||
|
|
||||||
|
Saving is done to a temporary directory, which is removed afterwards.
|
||||||
|
|
||||||
|
Returns the node.
|
||||||
|
"""
|
||||||
|
import tempfile
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
filepath = os.path.join(tmpdir, filename_on_cloud)
|
||||||
|
self.log.debug('Saving %s to %s', datablock, filepath)
|
||||||
|
datablock.save_render(filepath)
|
||||||
|
return await self.upload_file(filepath)
|
||||||
|
|
||||||
|
async def upload_file(self, filename: str, fileobj=None):
|
||||||
|
"""Uploads a file to the cloud, attached to the image sharing node.
|
||||||
|
|
||||||
|
Returns the node.
|
||||||
|
"""
|
||||||
|
from .. import blender
|
||||||
|
|
||||||
|
prefs = blender.preferences()
|
||||||
|
project = self.find_project(prefs.attract_project.project)
|
||||||
|
|
||||||
|
self.log.info('Uploading file %s', filename)
|
||||||
|
resp = await pillar.pillar_call(
|
||||||
|
pillarsdk.File.upload_to_project,
|
||||||
|
project['_id'],
|
||||||
|
'image/jpeg',
|
||||||
|
filename,
|
||||||
|
fileobj=fileobj)
|
||||||
|
|
||||||
|
self.log.debug('Returned data: %s', resp)
|
||||||
|
try:
|
||||||
|
file_id = resp['file_id']
|
||||||
|
except KeyError:
|
||||||
|
self.log.error('Upload did not succeed, response: %s', resp)
|
||||||
|
self.report({'ERROR'}, 'Unable to upload thumbnail to Attract: %s' % resp)
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.log.info('Created file %s', file_id)
|
||||||
|
self.report({'INFO'}, 'File succesfully uploaded to the cloud!')
|
||||||
|
|
||||||
|
return file_id
|
||||||
|
|
||||||
|
|
||||||
|
def draw_strip_movie_meta(self, context):
|
||||||
|
strip = active_strip(context)
|
||||||
|
if not strip:
|
||||||
|
return
|
||||||
|
|
||||||
|
meta = strip.get('metadata', None)
|
||||||
|
if not meta:
|
||||||
|
return None
|
||||||
|
|
||||||
|
box = self.layout.column(align=True)
|
||||||
|
row = box.row(align=True)
|
||||||
|
fname = meta.get('BLEND_FILE', None) or None
|
||||||
|
if fname:
|
||||||
|
row.label('Original Blendfile: %s' % fname)
|
||||||
|
row.operator(ATTRACT_OT_open_meta_blendfile.bl_idname,
|
||||||
|
text='', icon='FILE_BLEND')
|
||||||
|
sfra = meta.get('START_FRAME', '?')
|
||||||
|
efra = meta.get('END_FRAME', '?')
|
||||||
|
box.label('Original Frame Range: %s-%s' % (sfra, efra))
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
|
bpy.types.Sequence.atc_object_id_conflict = bpy.props.BoolProperty(
|
||||||
|
name='Object ID Conflict',
|
||||||
|
description='Attract Object ID used multiple times',
|
||||||
|
default=False)
|
||||||
bpy.types.Sequence.atc_name = bpy.props.StringProperty(name="Shot Name")
|
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_description = bpy.props.StringProperty(name="Shot Description")
|
||||||
bpy.types.Sequence.atc_notes = bpy.props.StringProperty(name="Shot notes")
|
bpy.types.Sequence.atc_notes = bpy.props.StringProperty(name="Shot Notes")
|
||||||
|
|
||||||
# TODO: get this from the project's node type definition.
|
# TODO: get this from the project's node type definition.
|
||||||
bpy.types.Sequence.atc_status = bpy.props.EnumProperty(
|
bpy.types.Sequence.atc_status = bpy.props.EnumProperty(
|
||||||
items=[
|
items=[
|
||||||
('on_hold', 'On hold', 'The shot is on hold'),
|
('on_hold', 'On Hold', 'The shot is on hold'),
|
||||||
('todo', 'Todo', 'Waiting'),
|
('todo', 'Todo', 'Waiting'),
|
||||||
('in_progress', 'In progress', 'The show has been assigned'),
|
('in_progress', 'In Progress', 'The show has been assigned'),
|
||||||
('review', 'Review', ''),
|
('review', 'Review', ''),
|
||||||
('final', 'Final', ''),
|
('final', 'Final', ''),
|
||||||
],
|
],
|
||||||
name="Status")
|
name="Status")
|
||||||
bpy.types.Sequence.atc_order = bpy.props.IntProperty(name="Order")
|
bpy.types.Sequence.atc_order = bpy.props.IntProperty(name="Order")
|
||||||
|
|
||||||
|
bpy.types.SEQUENCER_PT_edit.append(draw_strip_movie_meta)
|
||||||
|
|
||||||
bpy.utils.register_class(ToolsPanel)
|
bpy.utils.register_class(ToolsPanel)
|
||||||
bpy.utils.register_class(AttractShotSubmitNew)
|
|
||||||
bpy.utils.register_class(AttractShotRelink)
|
bpy.utils.register_class(AttractShotRelink)
|
||||||
bpy.utils.register_class(AttractShotSubmitUpdate)
|
|
||||||
bpy.utils.register_class(AttractShotDelete)
|
bpy.utils.register_class(AttractShotDelete)
|
||||||
bpy.utils.register_class(AttractStripUnlink)
|
bpy.utils.register_class(AttractStripUnlink)
|
||||||
bpy.utils.register_class(AttractShotFetchUpdate)
|
bpy.utils.register_class(AttractShotFetchUpdate)
|
||||||
bpy.utils.register_class(AttractShotSubmitSelected)
|
bpy.utils.register_class(AttractShotSubmitSelected)
|
||||||
|
bpy.utils.register_class(ATTRACT_OT_submit_all)
|
||||||
|
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.app.handlers.scene_update_post.append(scene_update_post_handler)
|
||||||
draw.callback_enable()
|
draw.callback_enable()
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
draw.callback_disable()
|
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_is_synced
|
||||||
del bpy.types.Sequence.atc_object_id
|
del bpy.types.Sequence.atc_object_id
|
||||||
|
del bpy.types.Sequence.atc_object_id_conflict
|
||||||
del bpy.types.Sequence.atc_name
|
del bpy.types.Sequence.atc_name
|
||||||
del bpy.types.Sequence.atc_description
|
del bpy.types.Sequence.atc_description
|
||||||
del bpy.types.Sequence.atc_notes
|
del bpy.types.Sequence.atc_notes
|
||||||
del bpy.types.Sequence.atc_status
|
del bpy.types.Sequence.atc_status
|
||||||
del bpy.types.Sequence.atc_order
|
del bpy.types.Sequence.atc_order
|
||||||
bpy.utils.unregister_module(__name__)
|
|
||||||
|
@@ -16,7 +16,7 @@
|
|||||||
#
|
#
|
||||||
# ##### END GPL LICENSE BLOCK #####
|
# ##### END GPL LICENSE BLOCK #####
|
||||||
|
|
||||||
# <pep8-80 compliant>
|
# <pep8 compliant>
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import logging
|
import logging
|
||||||
@@ -34,19 +34,22 @@ strip_status_colour = {
|
|||||||
'todo': (1.0, 0.5019607843137255, 0.5019607843137255)
|
'todo': (1.0, 0.5019607843137255, 0.5019607843137255)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CONFLICT_COLOUR = (0.576, 0.118, 0.035) # RGB tuple
|
||||||
|
|
||||||
def get_strip_rectf(strip, pixel_size_y):
|
|
||||||
|
def get_strip_rectf(strip):
|
||||||
# Get x and y in terms of the grid's frames and channels
|
# Get x and y in terms of the grid's frames and channels
|
||||||
x1 = strip.frame_final_start
|
x1 = strip.frame_final_start
|
||||||
x2 = strip.frame_final_end
|
x2 = strip.frame_final_end
|
||||||
y1 = strip.channel + 0.2 - pixel_size_y
|
y1 = strip.channel + 0.2
|
||||||
y2 = y1 + 2 * pixel_size_y
|
y2 = strip.channel - 0.2 + 1
|
||||||
|
|
||||||
return (x1, y1, x2, y2)
|
return x1, y1, x2, y2
|
||||||
|
|
||||||
|
|
||||||
def draw_underline_in_strip(strip_coords, pixel_size, color):
|
def draw_underline_in_strip(strip_coords, pixel_size_x, color):
|
||||||
from bgl import glColor4f, glRectf, glEnable, glDisable, GL_BLEND
|
from bgl import glColor4f, glRectf, glEnable, glDisable, GL_BLEND
|
||||||
|
import bgl
|
||||||
|
|
||||||
context = bpy.context
|
context = bpy.context
|
||||||
|
|
||||||
@@ -56,18 +59,44 @@ def draw_underline_in_strip(strip_coords, pixel_size, color):
|
|||||||
# be careful not to draw over the current frame line
|
# be careful not to draw over the current frame line
|
||||||
cf_x = context.scene.frame_current_final
|
cf_x = context.scene.frame_current_final
|
||||||
|
|
||||||
|
bgl.glPushAttrib(bgl.GL_COLOR_BUFFER_BIT | bgl.GL_LINE_BIT)
|
||||||
|
|
||||||
glColor4f(*color)
|
glColor4f(*color)
|
||||||
glEnable(GL_BLEND)
|
glEnable(GL_BLEND)
|
||||||
|
bgl.glLineWidth(2)
|
||||||
|
bgl.glBegin(bgl.GL_LINES)
|
||||||
|
|
||||||
|
bgl.glVertex2f(s_x1, s_y1)
|
||||||
if s_x1 < cf_x < s_x2:
|
if s_x1 < cf_x < s_x2:
|
||||||
# Bad luck, the line passes our strip
|
# Bad luck, the line passes our strip
|
||||||
glRectf(s_x1, s_y1, cf_x - pixel_size, s_y2)
|
bgl.glVertex2f(cf_x - pixel_size_x, s_y1)
|
||||||
glRectf(cf_x + pixel_size, s_y1, s_x2, s_y2)
|
bgl.glVertex2f(cf_x + pixel_size_x, s_y1)
|
||||||
else:
|
bgl.glVertex2f(s_x2, s_y1)
|
||||||
# Normal, full rectangle draw
|
|
||||||
glRectf(s_x1, s_y1, s_x2, s_y2)
|
|
||||||
|
|
||||||
glDisable(GL_BLEND)
|
bgl.glEnd()
|
||||||
|
bgl.glPopAttrib()
|
||||||
|
|
||||||
|
|
||||||
|
def draw_strip_conflict(strip_coords, pixel_size_x):
|
||||||
|
"""Draws conflicting states between strips."""
|
||||||
|
|
||||||
|
import bgl
|
||||||
|
|
||||||
|
s_x1, s_y1, s_x2, s_y2 = strip_coords
|
||||||
|
bgl.glPushAttrib(bgl.GL_COLOR_BUFFER_BIT | bgl.GL_LINE_BIT)
|
||||||
|
|
||||||
|
# Always draw the full rectangle, the conflict should be resolved and thus stand out.
|
||||||
|
bgl.glColor3f(*CONFLICT_COLOUR)
|
||||||
|
bgl.glLineWidth(2)
|
||||||
|
|
||||||
|
bgl.glBegin(bgl.GL_LINE_LOOP)
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
def draw_callback_px():
|
def draw_callback_px():
|
||||||
@@ -76,24 +105,22 @@ def draw_callback_px():
|
|||||||
if not context.scene.sequence_editor:
|
if not context.scene.sequence_editor:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
from . import shown_strips
|
||||||
|
|
||||||
region = context.region
|
region = context.region
|
||||||
xwin1, ywin1 = region.view2d.region_to_view(0, 0)
|
xwin1, ywin1 = region.view2d.region_to_view(0, 0)
|
||||||
xwin2, ywin2 = region.view2d.region_to_view(region.width, region.height)
|
xwin2, ywin2 = region.view2d.region_to_view(region.width, region.height)
|
||||||
one_pixel_further_x, one_pixel_further_y = region.view2d.region_to_view(1, 1)
|
one_pixel_further_x, one_pixel_further_y = region.view2d.region_to_view(1, 1)
|
||||||
pixel_size_x = one_pixel_further_x - xwin1
|
pixel_size_x = one_pixel_further_x - xwin1
|
||||||
pixel_size_y = one_pixel_further_y - ywin1
|
|
||||||
|
|
||||||
if context.scene.sequence_editor.meta_stack:
|
strips = shown_strips(context)
|
||||||
strips = context.scene.sequence_editor.meta_stack[-1].sequences
|
|
||||||
else:
|
|
||||||
strips = context.scene.sequence_editor.sequences
|
|
||||||
|
|
||||||
for strip in strips:
|
for strip in strips:
|
||||||
if not strip.atc_object_id:
|
if not strip.atc_object_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get corners (x1, y1), (x2, y2) of the strip rectangle in px region coords
|
# Get corners (x1, y1), (x2, y2) of the strip rectangle in px region coords
|
||||||
strip_coords = get_strip_rectf(strip, pixel_size_y)
|
strip_coords = get_strip_rectf(strip)
|
||||||
|
|
||||||
# check if any of the coordinates are out of bounds
|
# 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 \
|
if strip_coords[0] > xwin2 or strip_coords[2] < xwin1 or strip_coords[1] > ywin2 or \
|
||||||
@@ -109,7 +136,9 @@ def draw_callback_px():
|
|||||||
|
|
||||||
alpha = 1.0 if strip.atc_is_synced else 0.5
|
alpha = 1.0 if strip.atc_is_synced else 0.5
|
||||||
|
|
||||||
draw_underline_in_strip(strip_coords, pixel_size_x, color + (alpha, ))
|
draw_underline_in_strip(strip_coords, pixel_size_x, color + (alpha,))
|
||||||
|
if strip.atc_is_synced and strip.atc_object_id_conflict:
|
||||||
|
draw_strip_conflict(strip_coords, pixel_size_x)
|
||||||
|
|
||||||
|
|
||||||
def tag_redraw_all_sequencer_editors():
|
def tag_redraw_all_sequencer_editors():
|
||||||
|
@@ -31,8 +31,9 @@ import rna_prop_ui
|
|||||||
|
|
||||||
from . import pillar, async_loop
|
from . import pillar, async_loop
|
||||||
|
|
||||||
PILLAR_SERVER_URL = 'https://cloud.blender.org/api/'
|
PILLAR_WEB_SERVER_URL = 'https://cloud.blender.org/'
|
||||||
# PILLAR_SERVER_URL = 'http://pillar:5001/api/'
|
# PILLAR_WEB_SERVER_URL = 'http://pillar-web:5001/'
|
||||||
|
PILLAR_SERVER_URL = '%sapi/' % PILLAR_WEB_SERVER_URL
|
||||||
|
|
||||||
ADDON_NAME = 'blender_cloud'
|
ADDON_NAME = 'blender_cloud'
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -44,6 +45,36 @@ def redraw(self, context):
|
|||||||
context.area.tag_redraw()
|
context.area.tag_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
def pyside_cache(propname):
|
||||||
|
|
||||||
|
if callable(propname):
|
||||||
|
raise TypeError('Usage: pyside_cache("property_name")')
|
||||||
|
|
||||||
|
def decorator(wrapped):
|
||||||
|
"""Stores the result of the callable in Python-managed memory.
|
||||||
|
|
||||||
|
This is to work around the warning at
|
||||||
|
https://www.blender.org/api/blender_python_api_master/bpy.props.html#bpy.props.EnumProperty
|
||||||
|
"""
|
||||||
|
|
||||||
|
import functools
|
||||||
|
|
||||||
|
@functools.wraps(wrapped)
|
||||||
|
# We can't use (*args, **kwargs), because EnumProperty explicitly checks
|
||||||
|
# for the number of fixed positional arguments.
|
||||||
|
def wrapper(self, context):
|
||||||
|
result = None
|
||||||
|
try:
|
||||||
|
result = wrapped(self, context)
|
||||||
|
return result
|
||||||
|
finally:
|
||||||
|
rna_type, rna_info = getattr(self.bl_rna, propname)
|
||||||
|
rna_info['_cached_result'] = result
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
@pyside_cache('version')
|
||||||
def blender_syncable_versions(self, context):
|
def blender_syncable_versions(self, context):
|
||||||
"""Returns the list of items used by SyncStatusProperties.version EnumProperty."""
|
"""Returns the list of items used by SyncStatusProperties.version EnumProperty."""
|
||||||
|
|
||||||
@@ -106,6 +137,7 @@ class SyncStatusProperties(PropertyGroup):
|
|||||||
self['available_blender_versions'] = new_versions
|
self['available_blender_versions'] = new_versions
|
||||||
|
|
||||||
|
|
||||||
|
@pyside_cache('project')
|
||||||
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."""
|
||||||
|
|
||||||
@@ -165,7 +197,15 @@ class BlenderCloudPreferences(AddonPreferences):
|
|||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO: store local path with the Attract project, so that people
|
||||||
|
# can switch projects and the local path switches with it.
|
||||||
attract_project = PointerProperty(type=BlenderCloudProjectGroup)
|
attract_project = PointerProperty(type=BlenderCloudProjectGroup)
|
||||||
|
attract_project_local_path = StringProperty(
|
||||||
|
name='Local project path',
|
||||||
|
description='Local path of your Attract project, used to search for blend files; '
|
||||||
|
'usually best to set to an absolute path',
|
||||||
|
subtype='DIR_PATH',
|
||||||
|
default='//../')
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
import textwrap
|
import textwrap
|
||||||
@@ -245,15 +285,11 @@ class BlenderCloudPreferences(AddonPreferences):
|
|||||||
# Image Share stuff
|
# Image Share stuff
|
||||||
share_box = layout.box()
|
share_box = layout.box()
|
||||||
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.enabled = msg_icon != 'ERROR'
|
|
||||||
share_box.prop(self, 'open_browser_after_share')
|
share_box.prop(self, 'open_browser_after_share')
|
||||||
|
|
||||||
# Attract stuff
|
# Attract stuff
|
||||||
attract_box = layout.box()
|
attract_box = layout.box()
|
||||||
attract_box.enabled = msg_icon != 'ERROR'
|
self.draw_attract_buttons(attract_box, self.attract_project)
|
||||||
attract_row = attract_box.row(align=True)
|
|
||||||
attract_row.label('Attract', icon_value=icon('CLOUD'))
|
|
||||||
self.draw_attract_buttons(attract_row, self.attract_project)
|
|
||||||
|
|
||||||
def draw_subscribe_button(self, layout):
|
def draw_subscribe_button(self, layout):
|
||||||
layout.operator('pillar.subscribe', icon='WORLD')
|
layout.operator('pillar.subscribe', icon='WORLD')
|
||||||
@@ -289,9 +325,12 @@ class BlenderCloudPreferences(AddonPreferences):
|
|||||||
else:
|
else:
|
||||||
row_pull.label('Cloud Sync is running.')
|
row_pull.label('Cloud Sync is running.')
|
||||||
|
|
||||||
def draw_attract_buttons(self, layout, bcp: BlenderCloudProjectGroup):
|
def draw_attract_buttons(self, attract_box, bcp: BlenderCloudProjectGroup):
|
||||||
layout.enabled = bcp.status in {'NONE', 'IDLE'}
|
attract_row = attract_box.row(align=True)
|
||||||
row_buttons = layout.row(align=True)
|
attract_row.label('Attract', icon_value=icon('CLOUD'))
|
||||||
|
|
||||||
|
attract_row.enabled = bcp.status in {'NONE', 'IDLE'}
|
||||||
|
row_buttons = attract_row.row(align=True)
|
||||||
|
|
||||||
projects = bcp.available_projects
|
projects = bcp.available_projects
|
||||||
project = bcp.project
|
project = bcp.project
|
||||||
@@ -308,6 +347,8 @@ 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')
|
||||||
|
|
||||||
|
|
||||||
class PillarCredentialsUpdate(pillar.PillarOperatorMixin,
|
class PillarCredentialsUpdate(pillar.PillarOperatorMixin,
|
||||||
Operator):
|
Operator):
|
||||||
@@ -361,6 +402,7 @@ class PILLAR_OT_subscribe(Operator):
|
|||||||
"""Opens a browser to subscribe the user to the Cloud."""
|
"""Opens a browser to subscribe the user to the Cloud."""
|
||||||
bl_idname = 'pillar.subscribe'
|
bl_idname = 'pillar.subscribe'
|
||||||
bl_label = 'Subscribe to the Cloud'
|
bl_label = 'Subscribe to the Cloud'
|
||||||
|
bl_description = "Opens a page in a web browser to subscribe to the Blender Cloud"
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
import webbrowser
|
import webbrowser
|
||||||
@@ -374,7 +416,7 @@ class PILLAR_OT_subscribe(Operator):
|
|||||||
class PILLAR_OT_projects(async_loop.AsyncModalOperatorMixin,
|
class PILLAR_OT_projects(async_loop.AsyncModalOperatorMixin,
|
||||||
pillar.PillarOperatorMixin,
|
pillar.PillarOperatorMixin,
|
||||||
Operator):
|
Operator):
|
||||||
"""Fetches the projects available to the user, and ."""
|
"""Fetches the projects available to the user"""
|
||||||
bl_idname = 'pillar.projects'
|
bl_idname = 'pillar.projects'
|
||||||
bl_label = 'Fetch available projects'
|
bl_label = 'Fetch available projects'
|
||||||
|
|
||||||
|
@@ -218,10 +218,10 @@ 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 sync_call(pillar_func, *args, **kwargs):
|
def sync_call(pillar_func, *args, caching=True, **kwargs):
|
||||||
"""Synchronous call to Pillar, ensures the correct Api object is used."""
|
"""Synchronous call to Pillar, ensures the correct Api object is used."""
|
||||||
|
|
||||||
return pillar_func(*args, api=pillar_api(), **kwargs)
|
return pillar_func(*args, api=pillar_api(caching=caching), **kwargs)
|
||||||
|
|
||||||
|
|
||||||
async def check_pillar_credentials(required_roles: set):
|
async def check_pillar_credentials(required_roles: set):
|
||||||
|
@@ -553,6 +553,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
|
|||||||
"""Draws the GUI with OpenGL."""
|
"""Draws the GUI with OpenGL."""
|
||||||
|
|
||||||
drawers = {
|
drawers = {
|
||||||
|
'INITIALIZING': self._draw_initializing,
|
||||||
'CHECKING_CREDENTIALS': self._draw_checking_credentials,
|
'CHECKING_CREDENTIALS': self._draw_checking_credentials,
|
||||||
'BROWSING': self._draw_browser,
|
'BROWSING': self._draw_browser,
|
||||||
'DOWNLOADING_TEXTURE': self._draw_downloading,
|
'DOWNLOADING_TEXTURE': self._draw_downloading,
|
||||||
@@ -653,6 +654,13 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
|
|||||||
'Checking login credentials',
|
'Checking login credentials',
|
||||||
(0.0, 0.0, 0.2, 0.6))
|
(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):
|
def _draw_text_on_colour(self, context, text, bgcolour):
|
||||||
content_height, content_width = self._window_size(context)
|
content_height, content_width = self._window_size(context)
|
||||||
bgl.glEnable(bgl.GL_BLEND)
|
bgl.glEnable(bgl.GL_BLEND)
|
||||||
|
@@ -16,6 +16,8 @@
|
|||||||
#
|
#
|
||||||
# ##### END GPL LICENSE BLOCK #####
|
# ##### END GPL LICENSE BLOCK #####
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
|
||||||
def sizeof_fmt(num: int, suffix='B') -> str:
|
def sizeof_fmt(num: int, suffix='B') -> str:
|
||||||
"""Returns a human-readable size.
|
"""Returns a human-readable size.
|
||||||
@@ -29,3 +31,34 @@ def sizeof_fmt(num: int, suffix='B') -> str:
|
|||||||
num /= 1024
|
num /= 1024
|
||||||
|
|
||||||
return '%.1f Yi%s' % (num, suffix)
|
return '%.1f Yi%s' % (num, suffix)
|
||||||
|
|
||||||
|
|
||||||
|
def find_in_path(path: pathlib.Path, filename: str) -> pathlib.Path:
|
||||||
|
"""Performs a breadth-first search for the filename.
|
||||||
|
|
||||||
|
Returns the path that contains the file, or None if not found.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import collections
|
||||||
|
|
||||||
|
# Be lenient on our input type.
|
||||||
|
if isinstance(path, str):
|
||||||
|
path = pathlib.Path(path)
|
||||||
|
|
||||||
|
if not path.exists():
|
||||||
|
return None
|
||||||
|
assert path.is_dir()
|
||||||
|
|
||||||
|
to_visit = collections.deque([path])
|
||||||
|
while to_visit:
|
||||||
|
this_path = to_visit.popleft()
|
||||||
|
|
||||||
|
for subpath in this_path.iterdir():
|
||||||
|
if subpath.is_dir():
|
||||||
|
to_visit.append(subpath)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if subpath.name == filename:
|
||||||
|
return subpath
|
||||||
|
|
||||||
|
return None
|
||||||
|
8
requirements-dev.txt
Normal file
8
requirements-dev.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
-r requirements.txt
|
||||||
|
|
||||||
|
# Primary requirements
|
||||||
|
pytest==3.0.3
|
||||||
|
|
||||||
|
# Secondary requirements
|
||||||
|
py==1.4.31
|
||||||
|
|
@@ -1,7 +1,7 @@
|
|||||||
# Primary requirements:
|
# Primary requirements:
|
||||||
-e git+https://github.com/sybrenstuvel/cachecontrol.git@sybren-filecache-delete-crash-fix#egg=CacheControl
|
-e git+https://github.com/sybrenstuvel/cachecontrol.git@sybren-filecache-delete-crash-fix#egg=CacheControl
|
||||||
lockfile==0.12.2
|
lockfile==0.12.2
|
||||||
pillarsdk==1.6.0
|
pillarsdk==1.6.1
|
||||||
wheel==0.29.0
|
wheel==0.29.0
|
||||||
|
|
||||||
# Secondary requirements:
|
# Secondary requirements:
|
||||||
|
2
setup.py
2
setup.py
@@ -196,7 +196,7 @@ 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.4.99',
|
version='1.5.0',
|
||||||
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('.'),
|
||||||
|
25
tests/test_utils.py
Normal file
25
tests/test_utils.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""Unittests for blender_cloud.utils."""
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from blender_cloud import utils
|
||||||
|
|
||||||
|
|
||||||
|
class FindInPathTest(unittest.TestCase):
|
||||||
|
def test_nonexistant_path(self):
|
||||||
|
path = pathlib.Path('/doesnotexistreally')
|
||||||
|
self.assertFalse(path.exists())
|
||||||
|
self.assertIsNone(utils.find_in_path(path, 'jemoeder.blend'))
|
||||||
|
|
||||||
|
def test_really_breadth_first(self):
|
||||||
|
"""A depth-first test might find dir_a1/dir_a2/dir_a3/find_me.txt first."""
|
||||||
|
|
||||||
|
path = pathlib.Path(__file__).parent / 'test_really_breadth_first'
|
||||||
|
found = utils.find_in_path(path, 'find_me.txt')
|
||||||
|
self.assertEqual(path / 'dir_b1' / 'dir_b2' / 'find_me.txt', found)
|
||||||
|
|
||||||
|
def test_nonexistant_file(self):
|
||||||
|
path = pathlib.Path(__file__).parent / 'test_really_breadth_first'
|
||||||
|
found = utils.find_in_path(path, 'do_not_find_me.txt')
|
||||||
|
self.assertEqual(None, found)
|
15
update_version.sh
Executable file
15
update_version.sh
Executable 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!"
|
Reference in New Issue
Block a user