Compare commits

..

35 Commits

Author SHA1 Message Date
02b694f5d4 Bumped version to 1.10.0 and marked as released today 2019-01-02 16:19:23 +01:00
663ebae572 Bumped Blender-Asset-Tracer version to 0.8
This version has lots of Windows-specific fixes.
2019-01-02 16:19:05 +01:00
cb5a116dff Compatibility fix for Blender 2.8
bpy.context.user_preferences was renamed to bpy.context.preferences.
2018-12-28 12:31:33 +01:00
5821611d89 Compatibility fix with Blender 2.79 (Python 3.5) 2018-12-28 12:29:25 +01:00
8bd1faa575 Overwrite when deploying 2018-12-07 14:34:02 +01:00
8899bff5e4 Fixed Flamenco exclusion filter bug
There was a mistake in an older version of the property tooltip, showing
semicolon-separated instead of space-separated. We now just handle both.
2018-12-07 12:25:48 +01:00
4fd4ad7448 Added 'blender-video-chunks' job type
Requires that the file is configured for rendering to Matroska video
files.

Audio is only extracted when there is an audio codec configured. This is
a bit arbitrary, but it's at least a way to tell whether the artist is
considering that there is audio of any relevance in the current blend
file.
2018-12-07 11:28:09 +01:00
4f32b49ad3 Flamenco: Allow BAT-packing of only relative-path assets 2018-12-06 15:46:54 +01:00
1f13b4d249 Updated changelog 2018-12-05 13:01:03 +01:00
ef57dba5d3 Flamenco: Write more extensive information to jobinfo.json
This introduces version 2 of that file.

Version 1:
    - Only the job doc was saved, with 'missing_files' added inside it.

Version 2:
  - '_meta' key was added to indicate version.
  - 'job' is saved in a 'job' key, 'misssing_files' still top-level key.
  - 'exclusion_filter', 'project_settings', and
    'flamenco_manager_settings' keys were added.
2018-12-05 12:57:39 +01:00
419249ee19 Flamenco: Compress all blend files
All blend files in the BAT pack are now compressed, and not just the one
we save from Blender. Requires BAT 0.5 or newer.
2018-11-27 16:40:05 +01:00
113eb8f7ab Flamenco: add fps, output_file_extension, and images_or_video job settings
These are all needed to use FFmpeg on the worker to render a video from
rendered image sequences.

- fps: float, the scene FPS
- images_or_video: either 'images' or 'video', depending on what's being
  output by Blender. We don't support using FFmpeg to join chunked videos
  yet.
- output_file_extension: string like '.png' or '.exr', only set when
  outputting images (since doing this for video requires a lookup table and
  isn't even being used at the moment).
2018-11-21 14:24:32 +01:00
85f911cb59 Generalised saving/loading of project+manager-specific settings + added one
Added the `flamenco_exclude_filter` setting to the set, and also made it
easier to add new settings too.
2018-11-16 17:12:30 +01:00
564c2589b1 Added little script to automate deployment in Blender Animation Studio 2018-11-16 16:54:36 +01:00
80155ed4f4 Fixed storing & loading project+manager-specific settings
The problem was that there was too much storing done in an on-change
handler, causing things to be overwritten. By splitting up some functionality
and properly marking the "we're now loading" bits of code, its' solved.
2018-11-16 16:52:07 +01:00
d8c5c4eecd Cross-platformified my setup.py 'local' hack 2018-11-16 12:20:09 +01:00
3972ce4543 Write wheel files to correct dir in the bdist archive
They were ending up in a `local` directory next to the `blender_cloud`
directory. Probably something to do with newer setuptools? Had the same
issue in the Blender ID add-on.
2018-11-15 17:47:33 +01:00
d75a055149 Updated CHANGELOG 2018-11-12 15:07:03 +01:00
649542daad Prevent crashing Blender when running in the background 2018-11-12 15:02:51 +01:00
1d99751d20 Bumped version to 1.9.4 2018-11-01 18:39:24 +01:00
69028e0cfd Fixed Python 3.6 / 2.79b incompatibilities introduced in 1.9.3 2018-11-01 18:39:06 +01:00
dc7ad296bf Added little reminder for myself 2018-11-01 18:30:18 +01:00
3f2479067c Fixed incompatibility with Python 3.6 (used in Blender 2.79b) 2018-11-01 18:30:10 +01:00
6fefe4ffd8 Bumped version to 1.9.3 2018-10-30 14:17:02 +01:00
62c1c966f6 Attract: draw using the GPU module
The drawing is rather primitive, but it works.
2018-10-30 14:14:33 +01:00
57aadc1817 Attract: added 'open project in browser' button
The button was added to the video sequence editor panel.
2018-10-30 14:14:33 +01:00
7204d4a24c Added bl_category for Attract panel 2018-10-30 14:14:33 +01:00
641b51496a Some drawing code simplification 2018-10-30 14:14:33 +01:00
0562d57513 Attract: fixed class naming and registration 2018-10-30 14:14:33 +01:00
ac19e48895 Changelog update 2018-10-30 10:56:09 +01:00
73d96e5c89 Bumped version to 1.9.2 2018-09-17 18:58:05 +02:00
4bfdac223a Include Python 3.7-compatible pillarsdk 2018-09-17 18:57:57 +02:00
5d6777c74b Bumped version to 1.9.1 2018-09-17 18:47:52 +02:00
f4322f1d1f Updated changelog 2018-09-17 18:47:16 +02:00
13a8595cc0 Don't set prefs.flamenco_manager.manager to a not-in-the-enum value 2018-09-17 18:23:41 +02:00
16 changed files with 712 additions and 160 deletions

View File

@@ -1,5 +1,48 @@
# Blender Cloud changelog
## Version 1.10.0 (2019-01-02)
- Bundles Blender-Asset-Tracer 0.8.
- Fix crashing Blender when running in background mode (e.g. without GUI).
- Flamenco: Include extra job parameters to allow for encoding a video at the end of a render
job that produced an image sequence.
- Flamenco: Compress all blend files, and not just the one we save from Blender.
- Flamenco: Store more info in the `jobinfo.json` file. This is mostly useful for debugging issues
on the render farm, as now things like the exclusion filter and Manager settings are logged too.
- Flamenco: Allow BAT-packing of only those assets that are referred to by relative path (e.g.
a path starting with `//`). Assets with an absolute path are ignored, and assumed to be reachable
at the same path by the Workers.
- Flamenco: Added 'blender-video-chunks' job type, meant for rendering the edit of a film from the
VSE. This job type requires that the file is configured for rendering to Matroska video
files.
Audio is only extracted when there is an audio codec configured. This is a bit arbitrary, but it's
at least a way to tell whether the artist is considering that there is audio of any relevance in
the current blend file.
## Version 1.9.4 (2018-11-01)
- Fixed Python 3.6 and Blender 2.79b incompatibilities accidentally introduced in 1.9.3.
## Version 1.9.3 (2018-10-30)
- Fix drawing of Attract strips in the VSE on Blender 2.8.
## Version 1.9.2 (2018-09-17)
- No changes, just a different filename to force a refresh on our
hosting platform.
## Version 1.9.1 (2018-09-17)
- Fix issue with Python 3.7, which is used by current daily builds of Blender.
## Version 1.9 (2018-09-05)
- Last version to support Blender versions before 2.80!

View File

@@ -21,7 +21,7 @@
bl_info = {
'name': 'Blender Cloud',
"author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis",
'version': (1, 9, 0),
'version': (1, 10, 0),
'blender': (2, 80, 0),
'location': 'Addon Preferences panel, and Ctrl+Shift+Alt+A anywhere for texture browser',
'description': 'Texture library browser and Blender Sync. Requires the Blender ID addon '

View File

@@ -48,7 +48,12 @@ if "bpy" in locals():
async_loop = importlib.reload(async_loop)
blender = importlib.reload(blender)
else:
from . import draw
import bpy
if bpy.app.version < (2, 80):
from . import draw_27 as draw
else:
from . import draw
from .. import pillar, async_loop, blender
import bpy
@@ -174,10 +179,11 @@ class AttractPollMixin:
return attract_is_active
class AttractToolsPanel(AttractPollMixin, Panel):
class ATTRACT_PT_tools(AttractPollMixin, Panel):
bl_label = 'Attract'
bl_space_type = 'SEQUENCE_EDITOR'
bl_region_type = 'UI'
bl_category = 'Strip'
def draw_header(self, context):
strip = active_strip(context)
@@ -189,66 +195,69 @@ class AttractToolsPanel(AttractPollMixin, Panel):
layout = self.layout
strip_types = {'MOVIE', 'IMAGE', 'META', 'COLOR'}
selshots = list(selected_shots(context))
if strip and strip.type in strip_types and strip.atc_object_id:
if len(selshots) > 1:
noun = '%i Shots' % len(selshots)
else:
noun = 'This Shot'
if strip.atc_object_id_conflict:
warnbox = layout.box()
warnbox.alert = True
warnbox.label(text='Warning: This shot is linked to multiple sequencer strips.',
icon='ERROR')
layout.prop(strip, 'atc_name', text='Name')
layout.prop(strip, 'atc_status', text='Status')
# Create a special sub-layout for read-only properties.
ro_sub = layout.column(align=True)
ro_sub.enabled = False
ro_sub.prop(strip, 'atc_description', text='Description')
ro_sub.prop(strip, 'atc_notes', text='Notes')
if strip.atc_is_synced:
sub = layout.column(align=True)
row = sub.row(align=True)
if bpy.ops.attract.submit_selected.poll():
row.operator('attract.submit_selected',
text='Submit %s' % noun,
icon='TRIA_UP')
else:
row.operator(ATTRACT_OT_submit_all.bl_idname)
row.operator(AttractShotFetchUpdate.bl_idname,
text='', icon='FILE_REFRESH')
row.operator(ATTRACT_OT_shot_open_in_browser.bl_idname,
text='', icon='WORLD')
row.operator(ATTRACT_OT_copy_id_to_clipboard.bl_idname,
text='', icon='COPYDOWN')
sub.operator(ATTRACT_OT_make_shot_thumbnail.bl_idname,
text='Render Thumbnail for %s' % noun,
icon='RENDER_STILL')
# Group more dangerous operations.
dangerous_sub = layout.split(**blender.factor(0.6), align=True)
dangerous_sub.operator('attract.strip_unlink',
text='Unlink %s' % noun,
icon='PANEL_CLOSE')
dangerous_sub.operator(AttractShotDelete.bl_idname,
text='Delete %s' % noun,
icon='CANCEL')
self._draw_attractstrip_buttons(context, strip)
elif context.selected_sequences:
if len(context.selected_sequences) > 1:
noun = 'Selected Strips'
else:
noun = 'This Strip'
layout.operator(AttractShotSubmitSelected.bl_idname,
layout.operator(ATTRACT_OT_submit_selected.bl_idname,
text='Submit %s as New Shot' % noun)
layout.operator('attract.shot_relink')
else:
layout.operator(ATTRACT_OT_submit_all.bl_idname)
layout.operator(ATTRACT_OT_project_open_in_browser.bl_idname, icon='WORLD')
def _draw_attractstrip_buttons(self, context, strip):
"""Draw buttons when selected strips are Attract shots."""
layout = self.layout
selshots = list(selected_shots(context))
if len(selshots) > 1:
noun = '%i Shots' % len(selshots)
else:
noun = 'This Shot'
if strip.atc_object_id_conflict:
warnbox = layout.box()
warnbox.alert = True
warnbox.label(text='Warning: This shot is linked to multiple sequencer strips.',
icon='ERROR')
layout.prop(strip, 'atc_name', text='Name')
layout.prop(strip, 'atc_status', text='Status')
# Create a special sub-layout for read-only properties.
ro_sub = layout.column(align=True)
ro_sub.enabled = False
ro_sub.prop(strip, 'atc_description', text='Description')
ro_sub.prop(strip, 'atc_notes', text='Notes')
if strip.atc_is_synced:
sub = layout.column(align=True)
row = sub.row(align=True)
if bpy.ops.attract.submit_selected.poll():
row.operator('attract.submit_selected',
text='Submit %s' % noun,
icon='TRIA_UP')
else:
row.operator(ATTRACT_OT_submit_all.bl_idname)
row.operator(ATTRACT_OT_shot_fetch_update.bl_idname,
text='', icon='FILE_REFRESH')
row.operator(ATTRACT_OT_shot_open_in_browser.bl_idname,
text='', icon='WORLD')
row.operator(ATTRACT_OT_copy_id_to_clipboard.bl_idname,
text='', icon='COPYDOWN')
sub.operator(ATTRACT_OT_make_shot_thumbnail.bl_idname,
text='Render Thumbnail for %s' % noun,
icon='RENDER_STILL')
# Group more dangerous operations.
dangerous_sub = layout.split(**blender.factor(0.6), align=True)
dangerous_sub.operator('attract.strip_unlink',
text='Unlink %s' % noun,
icon='PANEL_CLOSE')
dangerous_sub.operator(ATTRACT_OT_shot_delete.bl_idname,
text='Delete %s' % noun,
icon='CANCEL')
class AttractOperatorMixin(AttractPollMixin):
@@ -379,7 +388,7 @@ class AttractOperatorMixin(AttractPollMixin):
draw.tag_redraw_all_sequencer_editors()
class AttractShotFetchUpdate(AttractOperatorMixin, Operator):
class ATTRACT_OT_shot_fetch_update(AttractOperatorMixin, Operator):
bl_idname = "attract.shot_fetch_update"
bl_label = "Fetch Update From Attract"
bl_description = 'Update status, description & notes from Attract'
@@ -398,7 +407,7 @@ class AttractShotFetchUpdate(AttractOperatorMixin, Operator):
return {'FINISHED'}
class AttractShotRelink(AttractShotFetchUpdate):
class ATTRACT_OT_shot_relink(AttractOperatorMixin, Operator):
bl_idname = "attract.shot_relink"
bl_label = "Relink With Attract"
@@ -467,7 +476,7 @@ class ATTRACT_OT_shot_open_in_browser(AttractOperatorMixin, Operator):
return {'FINISHED'}
class AttractShotDelete(AttractOperatorMixin, Operator):
class ATTRACT_OT_shot_delete(AttractOperatorMixin, Operator):
bl_idname = 'attract.shot_delete'
bl_label = 'Delete Shot'
bl_description = 'Remove this shot from Attract'
@@ -522,7 +531,7 @@ class AttractShotDelete(AttractOperatorMixin, Operator):
col.prop(self, 'confirm', text="I hereby confirm: delete %s from The Edit." % noun)
class AttractStripUnlink(AttractOperatorMixin, Operator):
class ATTRACT_OT_strip_unlink(AttractOperatorMixin, Operator):
bl_idname = 'attract.strip_unlink'
bl_label = 'Unlink Shot From This Strip'
bl_description = 'Remove Attract props from the selected strip(s)'
@@ -566,7 +575,7 @@ class AttractStripUnlink(AttractOperatorMixin, Operator):
return {'FINISHED'}
class AttractShotSubmitSelected(AttractOperatorMixin, Operator):
class ATTRACT_OT_submit_selected(AttractOperatorMixin, Operator):
bl_idname = 'attract.submit_selected'
bl_label = 'Submit All Selected'
bl_description = 'Submits all selected strips to Attract'
@@ -906,6 +915,37 @@ class ATTRACT_OT_copy_id_to_clipboard(AttractOperatorMixin, Operator):
return {'FINISHED'}
class ATTRACT_OT_project_open_in_browser(Operator):
bl_idname = 'attract.project_open_in_browser'
bl_label = 'Open Project in Browser'
bl_description = 'Opens a webbrowser to show the project in Attract'
project_id = bpy.props.StringProperty(name='Project ID', default='')
def execute(self, context):
import webbrowser
import urllib.parse
import pillarsdk
from ..pillar import sync_call
from ..blender import PILLAR_WEB_SERVER_URL, preferences
if not self.project_id:
self.project_id = preferences().project.project
project = sync_call(pillarsdk.Project.find, self.project_id, {'projection': {'url': True}})
if log.isEnabledFor(logging.DEBUG):
import pprint
log.debug('found project: %s', pprint.pformat(project.to_dict()))
url = urllib.parse.urljoin(PILLAR_WEB_SERVER_URL, 'attract/' + project.url)
webbrowser.open_new_tab(url)
self.report({'INFO'}, 'Opened a browser at %s' % url)
return {'FINISHED'}
def draw_strip_movie_meta(self, context):
strip = active_strip(context)
if not strip:
@@ -955,7 +995,9 @@ def deactivate():
pass
_rna_classes = [cls for cls in locals() if isinstance(cls, type) and hasattr(cls, 'bl_rna')]
_rna_classes = [cls for cls in locals().values()
if isinstance(cls, type) and cls.__name__.startswith('ATTRACT')]
log.info('RNA classes:\n%s', '\n'.join([repr(cls) for cls in _rna_classes]))
def register():

View File

@@ -18,9 +18,12 @@
# <pep8 compliant>
import bpy
import logging
import collections
import typing
import bpy
import bgl
import gpu
log = logging.getLogger(__name__)
@@ -34,10 +37,74 @@ strip_status_colour = {
'todo': (1.0, 0.5019607843137255, 0.5019607843137255)
}
CONFLICT_COLOUR = (0.576, 0.118, 0.035) # RGB tuple
CONFLICT_COLOUR = (0.576, 0.118, 0.035, 1.0) # RGBA tuple
gpu_vertex_shader = '''
uniform mat4 ModelViewProjectionMatrix;
layout (location = 0) in vec2 pos;
layout (location = 1) in vec4 color;
out vec4 lineColor; // output to the fragment shader
void main()
{
gl_Position = ModelViewProjectionMatrix * vec4(pos.x, pos.y, 0.0, 1.0);
lineColor = color;
}
'''
gpu_fragment_shader = '''
out vec4 fragColor;
in vec4 lineColor;
void main()
{
fragColor = lineColor;
}
'''
Float2 = typing.Tuple[float, float]
Float3 = typing.Tuple[float, float, float]
Float4 = typing.Tuple[float, float, float, float]
def get_strip_rectf(strip):
class AttractLineDrawer:
def __init__(self):
self._format = gpu.types.GPUVertFormat()
self._pos_id = self._format.attr_add(
id="pos",
comp_type="F32",
len=2,
fetch_mode="FLOAT")
self._color_id = self._format.attr_add(
id="color",
comp_type="F32",
len=4,
fetch_mode="FLOAT")
self.shader = gpu.types.GPUShader(gpu_vertex_shader, gpu_fragment_shader)
def draw(self,
coords: typing.List[Float2],
colors: typing.List[Float4]):
if not coords:
return
bgl.glEnable(bgl.GL_BLEND)
bgl.glLineWidth(2.0)
vbo = gpu.types.GPUVertBuf(len=len(coords), format=self._format)
vbo.attr_fill(id=self._pos_id, data=coords)
vbo.attr_fill(id=self._color_id, data=colors)
batch = gpu.types.GPUBatch(type="LINES", buf=vbo)
batch.program_set(self.shader)
batch.draw()
def get_strip_rectf(strip) -> Float4:
# Get x and y in terms of the grid's frames and channels
x1 = strip.frame_final_start
x2 = strip.frame_final_end
@@ -47,59 +114,56 @@ def get_strip_rectf(strip):
return x1, y1, x2, y2
def draw_underline_in_strip(strip_coords, pixel_size_x, color):
from bgl import glColor4f, glRectf, glEnable, glDisable, GL_BLEND
import bgl
context = bpy.context
def underline_in_strip(strip_coords: Float4,
pixel_size_x: float,
color: Float4,
out_coords: typing.List[Float2],
out_colors: typing.List[Float4]):
# Strip coords
s_x1, s_y1, s_x2, s_y2 = strip_coords
# be careful not to draw over the current frame line
cf_x = context.scene.frame_current_final
cf_x = bpy.context.scene.frame_current_final
bgl.glPushAttrib(bgl.GL_COLOR_BUFFER_BIT | bgl.GL_LINE_BIT)
# TODO(Sybren): figure out how to pass one colour per line,
# instead of one colour per vertex.
out_coords.append((s_x1, s_y1))
out_colors.append(color)
glColor4f(*color)
glEnable(GL_BLEND)
bgl.glLineWidth(2)
bgl.glBegin(bgl.GL_LINES)
bgl.glVertex2f(s_x1, s_y1)
if s_x1 < cf_x < s_x2:
# Bad luck, the line passes our strip
bgl.glVertex2f(cf_x - pixel_size_x, s_y1)
bgl.glVertex2f(cf_x + pixel_size_x, s_y1)
bgl.glVertex2f(s_x2, s_y1)
# Bad luck, the line passes our strip, so draw two lines.
out_coords.append((cf_x - pixel_size_x, s_y1))
out_colors.append(color)
bgl.glEnd()
bgl.glPopAttrib()
out_coords.append((cf_x + pixel_size_x, s_y1))
out_colors.append(color)
out_coords.append((s_x2, s_y1))
out_colors.append(color)
def draw_strip_conflict(strip_coords, pixel_size_x):
def strip_conflict(strip_coords: Float4,
out_coords: typing.List[Float2],
out_colors: typing.List[Float4]):
"""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)
# TODO(Sybren): draw a rectangle instead of a line.
out_coords.append((s_x1, s_y2))
out_colors.append(CONFLICT_COLOUR)
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()
out_coords.append((s_x2, s_y1))
out_colors.append(CONFLICT_COLOUR)
bgl.glPopAttrib()
out_coords.append((s_x2, s_y2))
out_colors.append(CONFLICT_COLOUR)
out_coords.append((s_x1, s_y1))
out_colors.append(CONFLICT_COLOUR)
def draw_callback_px():
def draw_callback_px(line_drawer: AttractLineDrawer):
context = bpy.context
if not context.scene.sequence_editor:
@@ -115,6 +179,10 @@ def draw_callback_px():
strips = shown_strips(context)
coords = [] # type: typing.List[Float2]
colors = [] # type: typing.List[Float4]
# Collect all the lines (vertex coords + vertex colours) to draw.
for strip in strips:
if not strip.atc_object_id:
continue
@@ -124,7 +192,7 @@ def draw_callback_px():
# 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:
strip_coords[3] < ywin1:
continue
# Draw
@@ -136,9 +204,11 @@ def draw_callback_px():
alpha = 1.0 if strip.atc_is_synced else 0.5
draw_underline_in_strip(strip_coords, pixel_size_x, color + (alpha,))
underline_in_strip(strip_coords, pixel_size_x, color + (alpha,), coords, colors)
if strip.atc_is_synced and strip.atc_object_id_conflict:
draw_strip_conflict(strip_coords, pixel_size_x)
strip_conflict(strip_coords, coords, colors)
line_drawer.draw(coords, colors)
def tag_redraw_all_sequencer_editors():
@@ -162,8 +232,13 @@ def callback_enable():
if cb_handle:
return
# Doing GPU stuff in the background crashes Blender, so let's not.
if bpy.app.background:
return
line_drawer = AttractLineDrawer()
cb_handle[:] = bpy.types.SpaceSequenceEditor.draw_handler_add(
draw_callback_px, (), 'WINDOW', 'POST_VIEW'),
draw_callback_px, (line_drawer,), 'WINDOW', 'POST_VIEW'),
tag_redraw_all_sequencer_editors()

View File

@@ -0,0 +1,182 @@
# ##### 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>
import bpy
import logging
import collections
log = logging.getLogger(__name__)
strip_status_colour = {
None: (0.7, 0.7, 0.7),
'approved': (0.6392156862745098, 0.8784313725490196, 0.30196078431372547),
'final': (0.9058823529411765, 0.9607843137254902, 0.8274509803921568),
'in_progress': (1.0, 0.7450980392156863, 0.0),
'on_hold': (0.796078431372549, 0.6196078431372549, 0.08235294117647059),
'review': (0.8941176470588236, 0.9607843137254902, 0.9764705882352941),
'todo': (1.0, 0.5019607843137255, 0.5019607843137255)
}
CONFLICT_COLOUR = (0.576, 0.118, 0.035) # RGB tuple
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 = strip.channel - 0.2 + 1
return x1, y1, x2, y2
def draw_underline_in_strip(strip_coords, pixel_size_x, color):
from bgl import glColor4f, glRectf, glEnable, glDisable, GL_BLEND
import bgl
context = bpy.context
# Strip coords
s_x1, s_y1, s_x2, s_y2 = strip_coords
# be careful not to draw over the current frame line
cf_x = context.scene.frame_current_final
bgl.glPushAttrib(bgl.GL_COLOR_BUFFER_BIT | bgl.GL_LINE_BIT)
glColor4f(*color)
glEnable(GL_BLEND)
bgl.glLineWidth(2)
bgl.glBegin(bgl.GL_LINES)
bgl.glVertex2f(s_x1, s_y1)
if s_x1 < cf_x < s_x2:
# Bad luck, the line passes our strip
bgl.glVertex2f(cf_x - pixel_size_x, s_y1)
bgl.glVertex2f(cf_x + pixel_size_x, s_y1)
bgl.glVertex2f(s_x2, s_y1)
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():
context = bpy.context
if not context.scene.sequence_editor:
return
from . import shown_strips
region = context.region
xwin1, ywin1 = region.view2d.region_to_view(0, 0)
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)
pixel_size_x = one_pixel_further_x - xwin1
strips = shown_strips(context)
for strip in strips:
if not strip.atc_object_id:
continue
# 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
status = strip.atc_status
if status in strip_status_colour:
color = strip_status_colour[status]
else:
color = strip_status_colour[None]
alpha = 1.0 if strip.atc_is_synced else 0.5
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():
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
try:
bpy.types.SpaceSequenceEditor.draw_handler_remove(cb_handle[0], 'WINDOW')
except ValueError:
# Thrown when already removed.
pass
cb_handle.clear()
tag_redraw_all_sequencer_editors()

View File

@@ -219,8 +219,8 @@ class BlenderCloudPreferences(AddonPreferences):
flamenco_manager = PointerProperty(type=flamenco.FlamencoManagerGroup)
flamenco_exclude_filter = StringProperty(
name='File Exclude Filter',
description='Filter like "*.abc;*.mkv" to prevent certain files to be packed '
'into the output directory',
description='Space-separated list of filename filters, like "*.abc *.mkv", to prevent '
'matching files from being packed into the output directory',
default='',
update=project_specific.store,
)
@@ -248,6 +248,15 @@ class BlenderCloudPreferences(AddonPreferences):
soft_max=4,
update=project_specific.store,
)
flamenco_relative_only = BoolProperty(
name='Relative Paths Only',
description='When enabled, only assets that are referred to with a relative path are '
'packed, and assets referred to by an absolute path are excluded from the '
'BAT pack. When disabled, all assets are packed.',
default=False,
update=project_specific.store,
)
flamenco_open_browser_after_submit = BoolProperty(
name='Open Browser after Submitting Job',
description='When enabled, Blender will open a webbrowser',
@@ -473,6 +482,7 @@ class BlenderCloudPreferences(AddonPreferences):
path_box.label(text='Blend file is not in your project path, '
'unable to give output path example.')
flamenco_box.prop(self, 'flamenco_relative_only')
flamenco_box.prop(self, 'flamenco_open_browser_after_submit')
@@ -652,8 +662,16 @@ class PILLAR_PT_image_custom_properties(rna_prop_ui.PropertyPanel, bpy.types.Pan
_property_type = bpy.types.Image
def ctx_preferences():
"""Returns bpy.context.preferences in a 2.79-compatible way."""
try:
return bpy.context.preferences
except AttributeError:
return bpy.context.user_preferences
def preferences() -> BlenderCloudPreferences:
return bpy.context.user_preferences.addons[ADDON_NAME].preferences
return ctx_preferences().addons[ADDON_NAME].preferences
def load_custom_icons():

View File

@@ -45,7 +45,7 @@ import bpy
from bpy.types import AddonPreferences, Operator, WindowManager, Scene, PropertyGroup
from bpy.props import StringProperty, EnumProperty, PointerProperty, BoolProperty, IntProperty
from .. import async_loop, pillar, project_specific
from .. import async_loop, pillar, project_specific, utils
from ..utils import pyside_cache, redraw
log = logging.getLogger(__name__)
@@ -53,6 +53,22 @@ log = logging.getLogger(__name__)
# Global flag used to determine whether panels etc. can be drawn.
flamenco_is_active = False
# 'image' file formats that actually produce a video.
VIDEO_FILE_FORMATS = {'FFMPEG', 'AVI_RAW', 'AVI_JPEG'}
# Video container name (from bpy.context.scene.render.ffmpeg.format) to file
# extension mapping. Any container name not listed here will be converted to
# lower case and prepended with a period. This is basically copied from
# Blender's source, get_file_extensions() in writeffmpeg.c.
VIDEO_CONTAINER_TO_EXTENSION = {
'QUICKTIME': '.mov',
'MPEG1': '.mpg',
'MPEG2': '.dvd',
'MPEG4': '.mp4',
'OGG': '.ogv',
'FLASH': '.flv',
}
@pyside_cache('manager')
def available_managers(self, context):
@@ -66,12 +82,35 @@ def available_managers(self, context):
return [(p['_id'], p['name'], '') for p in mngrs]
def manager_updated(self: 'FlamencoManagerGroup', context):
from ..blender import preferences
flamenco_manager_id = self.manager
log.debug('manager updated to %r', flamenco_manager_id)
prefs = preferences()
project_id = prefs.project.project
ps = prefs.get('project_settings', {}).get(project_id, {})
# Load per-project, per-manager settings for the current Manager.
try:
pppm = ps['flamenco_managers_settings'][flamenco_manager_id]
except KeyError:
# No settings for this manager, so nothing to do.
return
with project_specific.mark_as_loading():
project_specific.update_preferences(prefs,
project_specific.FLAMENCO_PER_PROJECT_PER_MANAGER,
pppm)
class FlamencoManagerGroup(PropertyGroup):
manager = EnumProperty(
items=available_managers,
name='Flamenco Manager',
description='Which Flamenco Manager to use for jobs',
update=project_specific.store,
update=manager_updated,
)
status = EnumProperty(
@@ -146,6 +185,18 @@ class FLAMENCO_OT_fmanagers(async_loop.AsyncModalOperatorMixin,
super().quit()
def guess_output_file_extension(output_format: str, scene) -> str:
"""Return extension, including period, like '.png' or '.mkv'."""
if output_format not in VIDEO_FILE_FORMATS:
return scene.render.file_extension
container = scene.render.ffmpeg.format
try:
return VIDEO_CONTAINER_TO_EXTENSION[container]
except KeyError:
return '.' + container.lower()
class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
pillar.AuthenticatedPillarOperatorMixin,
FlamencoPollMixin,
@@ -189,11 +240,6 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
return
self.log.info('Will output render files to %s', render_output)
# BAT-pack the files to the destination directory.
outdir, outfile, missing_sources = await self.bat_pack(filepath)
if not outfile:
return
# Fetch Manager for doing path replacement.
self.log.info('Going to fetch manager %s', self.user_id)
prefs = preferences()
@@ -207,15 +253,17 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
self.quit()
return
# Create the job at Flamenco Server.
context.window_manager.flamenco_status = 'COMMUNICATING'
# Construct as much of the job settings as we can before BAT-packing.
# Validation should happen as soon as possible (BAT-packing can take minutes).
frame_range = scene.flamenco_render_frame_range.strip() or scene_frame_range(context)
settings = {'blender_cmd': '{blender}',
'chunk_size': scene.flamenco_render_fchunk_size,
'filepath': manager.replace_path(outfile),
'frames': frame_range,
'render_output': manager.replace_path(render_output),
# Used for FFmpeg combining output frames into a video.
'fps': scene.render.fps / scene.render.fps_base,
'extract_audio': scene.render.ffmpeg.audio_codec != 'NONE',
}
# Add extra settings specific to the job type
@@ -232,9 +280,41 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
settings['cycles_sample_count'] = samples
settings['format'] = 'EXR'
# Let Flamenco Server know whether we'll output images or video.
output_format = settings.get('format') or scene.render.image_settings.file_format
if output_format in VIDEO_FILE_FORMATS:
settings['images_or_video'] = 'video'
else:
settings['images_or_video'] = 'images'
# Always pass the file format, even though it won't be
# necessary for the actual render command (the blend file
# already has the correct setting). It's used by other
# commands, such as FFmpeg combining output frames into
# a video.
#
# Note that this might be overridden above when the job type
# requires a specific file format.
settings.setdefault('format', scene.render.image_settings.file_format)
settings['output_file_extension'] = guess_output_file_extension(output_format, scene)
if not self.validate_job_settings(context, settings):
self.quit()
return
# BAT-pack the files to the destination directory.
outdir, outfile, missing_sources = await self.bat_pack(filepath)
if not outfile:
return
settings['filepath'] = manager.replace_path(outfile)
# Create the job at Flamenco Server.
context.window_manager.flamenco_status = 'COMMUNICATING'
project_id = prefs.project.project
try:
job_info = await create_job(self.user_id,
prefs.project.project,
project_id,
manager_id,
scene.flamenco_render_job_type,
settings,
@@ -250,8 +330,29 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
with open(str(outdir / 'jobinfo.json'), 'w', encoding='utf8') as outfile:
import json
job_info['missing_files'] = [str(mf) for mf in missing_sources]
json.dump(job_info, outfile, sort_keys=True, indent=4)
# Version 1: Only the job doc was saved, with 'missing_files' added inside it.
# Version 2:
# - '_meta' key was added to indicate version.
# - 'job' is saved in a 'job' key, 'misssing_files' still top-level key.
# - 'exclusion_filter', 'project_settings', and 'flamenco_manager_settings'
# keys were added.
project_settings = prefs.get('project_settings', {}).get(project_id, {})
if hasattr(project_settings, 'to_dict'):
project_settings = project_settings.to_dict()
# Pop out some settings so that settings of irrelevant Managers are excluded.
flamenco_managers_settings = project_settings.pop('flamenco_managers_settings', {})
flamenco_manager_settings = flamenco_managers_settings.pop(manager_id)
info = {
'_meta': {'version': 2},
'job': job_info,
'missing_files': [str(mf) for mf in missing_sources],
'exclusion_filter': (prefs.flamenco_exclude_filter or '').strip(),
'project_settings': project_settings,
'flamenco_manager_settings': flamenco_manager_settings,
}
json.dump(info, outfile, sort_keys=True, indent=4, cls=utils.JSONEncoder)
# We can now remove the local copy we made with bpy.ops.wm.save_as_mainfile().
# Strictly speaking we can already remove it after the BAT-pack, but it may come in
@@ -282,6 +383,26 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
self.quit()
def validate_job_settings(self, context, settings: dict) -> bool:
"""Perform settings validations for the selected job type.
:returns: True if ok, False if there was an error.
"""
job_type = context.scene.flamenco_render_job_type
if job_type == 'blender-video-chunks':
# This is not really a requirement, but should catch the mistake where it was
# left at the default setting (at the moment of writing that's 1 frame per chunk).
if context.scene.flamenco_render_fchunk_size < 10:
self.report({'ERROR'}, 'Job type requires chunks of at least 10 frames.')
return False
if settings['output_file_extension'] != '.mkv':
self.report({'ERROR'}, 'Job type requires rendering to Matroska files.')
return False
return True
def quit(self):
if bpy.context.window_manager.flamenco_status != 'ABORTED':
bpy.context.window_manager.flamenco_status = 'DONE'
@@ -348,6 +469,7 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
proj_abspath = bpy.path.abspath(prefs.cloud_project_local_path)
projdir = Path(proj_abspath).resolve()
exclusion_filter = (prefs.flamenco_exclude_filter or '').strip()
relative_only = prefs.flamenco_relative_only
self.log.debug('outdir : %s', outdir)
self.log.debug('projdir: %s', projdir)
@@ -362,7 +484,8 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
try:
outfile, missing_sources = await bat_interface.copy(
bpy.context, filepath, projdir, outdir, exclusion_filter)
bpy.context, filepath, projdir, outdir, exclusion_filter,
relative_only=relative_only)
except bat_interface.FileTransferError as ex:
self.log.error('Could not transfer %d files, starting with %s',
len(ex.files_remaining), ex.files_remaining[0])
@@ -584,12 +707,13 @@ def is_image_type(render_output_type: str) -> bool:
def _render_output_path(
local_project_path: str,
blend_filepath: Path,
flamenco_render_job_type: str,
flamenco_job_output_strip_components: int,
flamenco_job_output_path: str,
render_image_format: str,
flamenco_render_frame_range: str,
include_rel_path: bool = True,
) -> typing.Optional[PurePath]:
*,
include_rel_path: bool = True) -> typing.Optional[PurePath]:
"""Cached version of render_output_path()
This ensures that redraws of the Flamenco Render and Add-on preferences panels
@@ -621,6 +745,9 @@ def _render_output_path(
if stem.endswith('.flamenco'):
stem = stem[:-9]
if flamenco_render_job_type == 'blender-video-chunks':
return output_top / ('YYYY_MM_DD_SEQ-%s.mkv' % stem)
if include_rel_path:
rel_parts = proj_rel.parts[flamenco_job_output_strip_components:]
dir_components = output_top.joinpath(*rel_parts) / stem
@@ -658,6 +785,7 @@ def render_output_path(context, filepath: Path = None) -> typing.Optional[PurePa
return _render_output_path(
prefs.cloud_project_local_path,
filepath,
scene.flamenco_render_job_type,
prefs.flamenco_job_output_strip_components,
job_output_path,
scene.render.image_settings.file_format,
@@ -854,6 +982,8 @@ def register():
('blender-render', 'Simple Render', 'Simple frame-by-frame render'),
('blender-render-progressive', 'Progressive Render',
'Each frame is rendered multiple times with different Cycles sample chunks, then combined'),
('blender-video-chunks', 'Video Chunks',
'Render each frame chunk to a video file, then concateate those video files')
]
)

View File

@@ -2,15 +2,15 @@
import asyncio
import logging
import pathlib
import re
import threading
import typing
import pathlib
import bpy
from blender_asset_tracer import pack
from blender_asset_tracer.pack import progress, transfer
log = logging.getLogger(__name__)
_running_packer = None # type: pack.Packer
@@ -97,7 +97,9 @@ async def copy(context,
base_blendfile: pathlib.Path,
project: pathlib.Path,
target: pathlib.Path,
exclusion_filter: str) -> typing.Tuple[pathlib.Path, typing.Set[pathlib.Path]]:
exclusion_filter: str,
*,
relative_only: bool) -> typing.Tuple[pathlib.Path, typing.Set[pathlib.Path]]:
"""Use BAT🦇 to copy the given file and dependencies to the target location.
:raises: FileTransferError if a file couldn't be transferred.
@@ -109,10 +111,15 @@ async def copy(context,
wm = bpy.context.window_manager
with pack.Packer(base_blendfile, project, target) as packer:
with pack.Packer(base_blendfile, project, target, compress=True, relative_only=relative_only) \
as packer:
with _packer_lock:
if exclusion_filter:
packer.exclude(*exclusion_filter.split())
# There was a mistake in an older version of the property tooltip,
# showing semicolon-separated instead of space-separated. We now
# just handle both.
filter_parts = re.split('[ ;]+', exclusion_filter.strip(' ;'))
packer.exclude(*filter_parts)
packer.progress_cb = BatProgress()
_running_packer = packer

View File

@@ -2,6 +2,7 @@
import contextlib
import logging
import typing
# Names of BlenderCloudPreferences properties that are both project-specific
# and simple enough to store directly in a dict.
@@ -9,19 +10,53 @@ PROJECT_SPECIFIC_SIMPLE_PROPS = (
'cloud_project_local_path',
)
# Names of BlenderCloudPreferences properties that are project-specific and
# Flamenco Manager-specific, and simple enough to store in a dict.
FLAMENCO_PER_PROJECT_PER_MANAGER = (
'flamenco_exclude_filter',
'flamenco_job_file_path',
'flamenco_job_output_path',
'flamenco_job_output_strip_components',
'flamenco_relative_only',
)
log = logging.getLogger(__name__)
project_settings_loading = False
project_settings_loading = 0 # counter, if > 0 then we're loading stuff.
@contextlib.contextmanager
def mark_as_loading():
"""Sets project_settings_loading=True while the context is active."""
"""Sets project_settings_loading > 0 while the context is active.
A counter is used to allow for nested mark_as_loading() contexts.
"""
global project_settings_loading
project_settings_loading = True
project_settings_loading += 1
try:
yield
finally:
project_settings_loading = False
project_settings_loading -= 1
def update_preferences(prefs, names_to_update: typing.Iterable[str],
new_values: typing.Mapping[str, typing.Any]):
for name in names_to_update:
if not hasattr(prefs, name):
log.debug('not setting %r, property cannot be found', name)
continue
if name in new_values:
log.debug('setting %r = %r', name, new_values[name])
setattr(prefs, name, new_values[name])
else:
# The property wasn't stored, so set the default value instead.
bl_type, args = getattr(prefs.bl_rna, name)
log.debug('finding default value for %r', name)
if 'default' not in args:
log.debug('no default value for %r, not touching', name)
continue
log.debug('found default value for %r = %r', name, args['default'])
setattr(prefs, name, args['default'])
def handle_project_update(_=None, _2=None):
@@ -66,9 +101,7 @@ def handle_project_update(_=None, _2=None):
log.debug('loading project-specific settings:\n%s', pformat(ps.to_dict()))
# Restore simple properties.
for name in PROJECT_SPECIFIC_SIMPLE_PROPS:
if name in ps and hasattr(prefs, name):
setattr(prefs, name, ps[name])
update_preferences(prefs, PROJECT_SPECIFIC_SIMPLE_PROPS, ps)
# Restore Flamenco settings.
prefs.flamenco_manager.available_managers = ps.get('flamenco_available_managers', [])
@@ -76,23 +109,12 @@ def handle_project_update(_=None, _2=None):
if flamenco_manager_id:
log.debug('setting flamenco manager to %s', flamenco_manager_id)
try:
# This will trigger a load of Project+Manager-specfic settings.
prefs.flamenco_manager.manager = flamenco_manager_id
except TypeError:
log.warning('manager %s for this project could not be found', flamenco_manager_id)
else:
# Load per-project, per-manager settings for the current Manager.
try:
pppm = ps['flamenco_managers_settings'][flamenco_manager_id]
except KeyError:
# No settings for this manager, so nothing to do.
pass
else:
prefs.flamenco_job_file_path = pppm['file_path']
prefs.flamenco_job_output_path = pppm['output_path']
prefs.flamenco_job_output_strip_components = pppm['output_strip_components']
else:
log.debug('Resetting Flamenco Manager to None')
prefs.flamenco_manager.manager = None
elif prefs.flamenco_manager.available_managers:
prefs.flamenco_manager.manager = prefs.flamenco_manager.available_managers[0]
def store(_=None, _2=None):
@@ -124,9 +146,8 @@ def store(_=None, _2=None):
# Store per-project, per-manager settings for the current Manager.
pppm = ps.get('flamenco_managers_settings', {})
pppm[prefs.flamenco_manager.manager] = {
'file_path': prefs.flamenco_job_file_path,
'output_path': prefs.flamenco_job_output_path,
'output_strip_components': prefs.flamenco_job_output_strip_components}
name: getattr(prefs, name) for name in FLAMENCO_PER_PROJECT_PER_MANAGER
}
ps['flamenco_managers_settings'] = pppm # IDPropertyGroup has no setdefault() method.
# Store this project's settings in the preferences.

View File

@@ -34,7 +34,7 @@ import asyncio
import pillarsdk
from pillarsdk import exceptions as sdk_exceptions
from .pillar import pillar_call
from . import async_loop, pillar, cache, blendfile, home_project
from . import async_loop, blender, pillar, cache, blendfile, home_project
SETTINGS_FILES_TO_UPLOAD = ['userpref.blend', 'startup.blend']
@@ -476,13 +476,13 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin,
self.log.info('Overriding machine-local settings in %s', file_path)
# Remember some settings that should not be overwritten from the Cloud.
up = bpy.context.user_preferences
prefs = blender.ctx_preferences()
remembered = {}
for rna_key, python_key in LOCAL_SETTINGS_RNA:
assert '.' in python_key, 'Sorry, this code assumes there is a dot in the Python key'
try:
value = up.path_resolve(python_key)
value = prefs.path_resolve(python_key)
except ValueError:
# Setting doesn't exist. This can happen, for example Cycles
# settings on a build that doesn't have Cycles enabled.
@@ -491,7 +491,7 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin,
# Map enums from strings (in Python) to ints (in DNA).
dot_index = python_key.rindex('.')
parent_key, prop_key = python_key[:dot_index], python_key[dot_index + 1:]
parent = up.path_resolve(parent_key)
parent = prefs.path_resolve(parent_key)
prop = parent.bl_rna.properties[prop_key]
if prop.type == 'ENUM':
log.debug('Rewriting %s from %r to %r',

View File

@@ -244,7 +244,7 @@ class MenuItem:
# draw some text
font_id = 0
text_dpi = bpy.context.user_preferences.system.dpi
text_dpi = blender.ctx_preferences().system.dpi
text_x = self.x + self.icon_margin_x + ICON_WIDTH + self.text_margin_x
text_y = self.y + ICON_HEIGHT * 0.5 - 0.25 * self.text_size
blf.position(font_id, text_x, text_y, 0)

View File

@@ -16,6 +16,7 @@
#
# ##### END GPL LICENSE BLOCK #####
import json
import pathlib
@@ -102,3 +103,12 @@ def redraw(self, context):
if context.area is None:
return
context.area.tag_redraw()
class JSONEncoder(json.JSONEncoder):
"""JSON encoder with support for some Blender types."""
def default(self, o):
if o.__class__.__name__ == 'IDPropertyGroup' and hasattr(o, 'to_dict'):
return o.to_dict()
return super().default(o)

13
deploy-to-shared.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash -e
FULLNAME="$(python3 setup.py --fullname)"
echo "Press [ENTER] to deploy $FULLNAME to /shared"
read dummy
./clear_wheels.sh
python3 setup.py wheels bdist
DISTDIR=$(pwd)/dist
cd /shared/software/addons
rm -vf blender_cloud/wheels/*.whl # remove obsolete wheel files
unzip -o $DISTDIR/$FULLNAME.addon.zip

View File

@@ -1,9 +1,9 @@
# Primary requirements:
-e git+https://github.com/sybrenstuvel/cachecontrol.git@sybren-filecache-delete-crash-fix#egg=CacheControl
lockfile==0.12.2
pillarsdk==1.6.1
pillarsdk==1.7.0
wheel==0.29.0
blender-asset-tracer>=0.4
blender-asset-tracer>=0.8
# Secondary requirements:
asn1crypto==0.24.0

View File

@@ -28,7 +28,7 @@ import zipfile
from distutils import log
from distutils.core import Command
from distutils.command.bdist import bdist
from distutils.command.install import install
from distutils.command.install import install, INSTALL_SCHEMES
from distutils.command.install_egg_info import install_egg_info
from setuptools import setup, find_packages
@@ -165,6 +165,14 @@ class BlenderAddonBdist(bdist):
super().initialize_options()
self.formats = ['zip']
self.plat_name = 'addon' # use this instead of 'linux-x86_64' or similar.
self.fix_local_prefix()
def fix_local_prefix(self):
"""Place data files in blender_cloud instead of local/blender_cloud."""
for key in INSTALL_SCHEMES:
if 'data' not in INSTALL_SCHEMES[key]:
continue
INSTALL_SCHEMES[key]['data'] = '$base'
def run(self):
self.run_command('wheels')
@@ -228,7 +236,7 @@ setup(
'wheels': BuildWheels},
name='blender_cloud',
description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.',
version='1.9.0',
version='1.10.0',
author='Sybren A. Stüvel',
author_email='sybren@stuvel.eu',
packages=find_packages('.'),

View File

@@ -17,3 +17,6 @@ echo
echo "Don't forget to commit and tag:"
echo git commit -m \'Bumped version to $VERSION\' setup.py blender_cloud/__init__.py
echo git tag -a version-$VERSION -m \'Tagged version $VERSION\'
echo
echo "To build a distribution ZIP:"
echo python setup.py bdist