Integrating data system; unit tests in Blender

This commit is contained in:
Terry Hancock 2021-05-28 11:54:09 -05:00
parent 48841cc05b
commit 0d81cc5857
28 changed files with 4470 additions and 299 deletions

View File

@ -1,11 +0,0 @@
#script to run:
SCRIPT="/project/terry/Dev/eclipse-workspace/ABX/src/abx.py"
#path to the PyDev folder that contains a file named pydevd.py:
PYDEVD_PATH='/home/terry/.eclipse/360744294_linux_gtk_x86_64/plugins/org.python.pydev.core_7.3.0.201908161924/pysrc/'
#PYDEVD_PATH='/home/terry/.config/blender/2.79/scripts/addons/modules/pydev_debug.py'
import pydev_debug as pydev #pydev_debug.py is in a folder from Blender PYTHONPATH
pydev.debug(SCRIPT, PYDEVD_PATH, trace = True)

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python
# Run the script in the debugger client within Blender:
import subprocess
subprocess.call(['blender', '-P', '/project/terry/Dev/eclipse-workspace/ABX/BlenderRemoteDebug.py'])
subprocess.call(['blender279', '-P', '/project/terry/Dev/Git/abx/scripts/BlenderRemoteDebug.py'])

4
TestInBlender.py Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env python
# Inject the unittest runner script into Blender and run it in batch mode:
import subprocess
subprocess.call(['blender279', '-b', '-P', '/project/terry/Dev/Git/abx/scripts/TestInBlender_bpy.py'])

View File

@ -2,7 +2,7 @@
bl_info = {
"name": "ABX",
"author": "Terry Hancock / Lunatics.TV Project / Anansi Spaceworks",
"version": (0, 2, 5),
"version": (0, 2, 6),
"blender": (2, 79, 0),
"location": "SpaceBar Search -> ABX",
"description": "Anansi Studio Extensions for Blender",
@ -22,17 +22,20 @@ try:
except ImportError:
print("Blender Add-On 'ABX' requires the Blender Python environment to run.")
print("blender_present = ", blender_present)
if blender_present:
if blender_present:
from . import abx_ui
BlendFile = abx_ui.BlendFile
def register():
bpy.utils.register_module(__name__)
abx_ui.register()
#bpy.utils.register_module(__name__)
def unregister():
bpy.utils.unregister_module(__name__)
abx_ui.unregister()
#bpy.utils.unregister_module(__name__)
if __name__ == "__main__":
register()

View File

@ -1,72 +1,113 @@
# DEFAULT ABX SETTINGS
---
project_schema:
- rank: project
delimiter: '-'
words: True
type: string
- rank: sequence
type:
VN: Vague Name
- rank: shot
type: letter
maxlength: 1
- rank: element
type: string
maxlength: 2
abx_default: True
render_profiles:
previz:
name: PreViz,
desc: 'GL/AVI Previz Render for Animatics',
engine: gl
version: any
fps: 30
fps_div: 1000
fps_skip: 1
suffix: GL
format: AVI_JPEG
extension: avi
freestyle: False
project_unit: []
project_schema: []
definitions:
filetypes:
blend: "Blender File"
kdenlive: "Kdenlive Video Editor File"
mlt: "Kdenlive Video Mix Script"
svg: "Scalable Vector Graphics (Inkscape)"
kra: "Krita Graphic File"
xcf: "Gimp Graphic File"
png: "Portable Network Graphics (PNG) Image"
jpg: "Joint Photographic Experts Group (JPEG) Image"
aup: "Audacity Project"
ardour: "Ardour Project"
flac: "Free Lossless Audio Codec (FLAC)"
mp3: "MPEG Audio Layer III (MP3) Audio File"
ogg: "Ogg Vorbis Audio File"
avi: "Audio Video Interleave (AVI) Video Container"
mkv: "Matroska Video Container"
mp4: "Moving Picture Experts Group (MPEG) 4 Format"
txt: "Plain Text File"
quick:
name: 30fps Paint
desc: '30fps Simplified Paint-Only Render'
engine: bi
fps: 30
fps_skip: 3
suffix: PT
format: AVI_JPEG
extension: avi
freestyle: False,
antialias: False,
motionblur: False
roles:
extras: "Extras, crowds, auxillary animated movement"
mech: "Mechanical animation"
anim: "Character animation"
cam: "Camera direction"
vfx: "Visual special effects"
compos: "Compositing"
bkg: "Background 2D image"
bb: "Billboard 2D image"
tex: "Texture 2D image"
foley: "Foley sound"
voice: "Voice recording"
fx: "Sound effects"
music: "Music track"
cue: "Musical cue"
amb: "Ambient sound"
loop: "Ambient sound loop"
edit: "Video edit"
roles_by_filetype:
kdenlive: edit
mlt: edit
omit_ranks: # Controls how much we shorten names
edit: 0
render: 0
filename: 0
scene: 0
abx:
render_profiles:
previz:
name: PreViz,
desc: 'GL/AVI Previz Render for Animatics'
engine: gl
version: any
fps: 30
fps_div: 1000
fps_skip: 1
suffix: GL
format: AVI_JPEG
extension: avi
freestyle: False
check:
name: 1fps Check
desc: '1fps Full-Features Check Renders'
engine: bi
fps: 30
fps_skip: 30
suffix: CH
format: JPEG
extension: jpg
framedigits: 5
freestyle: True
antialias: 8
full:
name: 30fps Full
desc: 'Full Render with all Features Turned On',
engine: bi
fps: 30
fps_skip: 1
suffix: ''
format: PNG
extension: png
framedigits: 5
freestyle: True
antialias: 8
quick:
name: 30fps Paint
desc: '30fps Simplified Paint-Only Render'
engine: bi
fps: 30
fps_skip: 3
suffix: PT
format: AVI_JPEG
extension: avi
freestyle: False,
antialias: False,
motionblur: False
check:
name: 1fps Check
desc: '1fps Full-Features Check Renders'
engine: bi
fps: 30
fps_skip: 30
suffix: CH
format: JPEG
extension: jpg
framedigits: 5
freestyle: True
antialias: 8
full:
name: 30fps Full
desc: 'Full Render with all Features Turned On'
engine: bi
fps: 30
fps_skip: 1
suffix: ''
format: PNG
extension: png
framedigits: 5
freestyle: True
antialias: 8
motionblur: 2
rendersize: 100
compress: 50

View File

@ -23,14 +23,22 @@ run into.
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
import os
import bpy, bpy.utils, bpy.types, bpy.props
from bpy.app.handlers import persistent
from . import file_context
# if bpy.data.filepath:
# BlendfileContext = file_context.FileContext(bpy.data.filepath)
# else:
# BlendfileContext = file_context.FileContext()
#
# abx_data = BlendfileContext.abx_data
from . import copy_anim
from . import std_lunatics_ink
from abx import ink_paint
from . import render_profile
@ -273,8 +281,7 @@ class LunaticsSceneProperties(bpy.types.PropertyGroup):
maxlen=0
)
bpy.utils.register_class(LunaticsSceneProperties)
bpy.types.Scene.lunaprops = bpy.props.PointerProperty(type=LunaticsSceneProperties)
class LunaticsScenePanel(bpy.types.Panel):
"""
@ -315,9 +322,7 @@ class RenderProfileSettings(bpy.types.PropertyGroup):
description="Select from pre-defined profiles of render settings",
default='full')
bpy.utils.register_class(RenderProfileSettings)
bpy.types.Scene.render_profile_settings = bpy.props.PointerProperty(
type=RenderProfileSettings)
class RenderProfilesOperator(bpy.types.Operator):
"""
@ -335,6 +340,7 @@ class RenderProfilesOperator(bpy.types.Operator):
return {'FINISHED'}
class RenderProfilesPanel(bpy.types.Panel):
"""
Add simple drop-down selector for generating common render settings with
@ -354,6 +360,7 @@ class RenderProfilesPanel(bpy.types.Panel):
row.operator('render.render_profiles')
class copy_animation(bpy.types.Operator):
"""
Copy animation from active object to selected objects (select source last!).
@ -399,6 +406,8 @@ class copy_animation(bpy.types.Operator):
return {'FINISHED'}
class copy_animation_settings(bpy.types.PropertyGroup):
"""
Settings for the 'copy_animation' operator.
@ -423,8 +432,7 @@ class copy_animation_settings(bpy.types.PropertyGroup):
description = "Scale factor for scaling animation (Re-Scale w/ 1.0 copies actions)",
default = 1.0)
bpy.utils.register_class(copy_animation_settings)
bpy.types.Scene.copy_anim_settings = bpy.props.PointerProperty(type=copy_animation_settings)
class CharacterPanel(bpy.types.Panel):
bl_space_type = "VIEW_3D" # window type panel is displayed in
@ -443,7 +451,7 @@ class CharacterPanel(bpy.types.Panel):
layout.prop(settings, 'rescale')
layout.prop(settings, 'scale_factor')
class lunatics_compositing_settings(bpy.types.PropertyGroup):
@ -465,9 +473,7 @@ class lunatics_compositing_settings(bpy.types.PropertyGroup):
description = "Render sky separately with compositing support (better shadows)",
default = True)
bpy.utils.register_class(lunatics_compositing_settings)
bpy.types.Scene.lx_compos_settings = bpy.props.PointerProperty(type=lunatics_compositing_settings)
class lunatics_compositing(bpy.types.Operator):
"""
Set up standard Lunatics scene compositing.
@ -483,7 +489,7 @@ class lunatics_compositing(bpy.types.Operator):
"""
scene = context.scene
shot = std_lunatics_ink.LunaticsShot(scene,
shot = ink_paint.LunaticsShot(scene,
inkthru=context.scene.lx_compos_settings.inkthru,
billboards=context.scene.lx_compos_settings.billboards,
sepsky=context.scene.lx_compos_settings.sepsky )
@ -497,7 +503,7 @@ class lunatics_compositing(bpy.types.Operator):
# self.col = self.layout.col()
# col.prop(settings, "inkthru", text="Ink Thru")
# col.prop(settings, "billboards", text="Ink Thru")
class LunaticsPanel(bpy.types.Panel):
@ -515,16 +521,52 @@ class LunaticsPanel(bpy.types.Panel):
layout.prop(settings, 'inkthru', text="Ink-Thru")
layout.prop(settings, 'billboards', text="Billboards")
layout.prop(settings, 'sepsky', text="Separate Sky")
BlendFile = file_context.FileContext()
@persistent
def update_handler(ctxt):
BlendFile.update(bpy.data.filepath)
def register():
bpy.utils.register_module(__name__)
bpy.utils.register_class(LunaticsSceneProperties)
bpy.types.Scene.lunaprops = bpy.props.PointerProperty(type=LunaticsSceneProperties)
bpy.utils.register_class(LunaticsScenePanel)
bpy.utils.register_class(RenderProfileSettings)
bpy.types.Scene.render_profile_settings = bpy.props.PointerProperty(
type=RenderProfileSettings)
bpy.utils.register_class(RenderProfilesOperator)
bpy.utils.register_class(RenderProfilesPanel)
bpy.utils.register_class(copy_animation)
bpy.utils.register_class(copy_animation_settings)
bpy.types.Scene.copy_anim_settings = bpy.props.PointerProperty(type=copy_animation_settings)
bpy.utils.register_class(CharacterPanel)
bpy.utils.register_class(lunatics_compositing_settings)
bpy.types.Scene.lx_compos_settings = bpy.props.PointerProperty(type=lunatics_compositing_settings)
bpy.utils.register_class(lunatics_compositing)
bpy.utils.register_class(LunaticsPanel)
bpy.app.handlers.save_post.append(update_handler)
bpy.app.handlers.load_post.append(update_handler)
bpy.app.handlers.scene_update_post.append(update_handler)
def unregister():
bpy.utils.unregister_module(__name__)
bpy.utils.unregister_class(LunaticsSceneProperties)
bpy.utils.unregister_class(LunaticsScenePanel)
if __name__ == "__main__":
register()
bpy.utils.unregister_class(RenderProfileSettings)
bpy.utils.unregister_class(RenderProfilesOperator)
bpy.utils.unregister_class(RenderProfilesPanel)
bpy.utils.unregister_class(copy_animation)
bpy.utils.unregister_class(copy_animation_settings)
bpy.utils.unregister_class(CharacterPanel)
bpy.utils.unregister_class(lunatics_compositing_settings)
bpy.utils.unregister_class(lunatics_compositing)
bpy.utils.unregister_class(LunaticsPanel)

View File

@ -244,6 +244,10 @@ class RecursiveDict(collections.OrderedDict):
#--------
# Code for collecting the YAML files we need
ABX_YAML = os.path.join(os.path.dirname(
os.path.abspath(os.path.join(__file__))),
'abx.yaml')
def collect_yaml_files(path, stems, dirmatch=False, sidecar=False, root='/'):
"""
@ -327,8 +331,10 @@ def get_project_data(filepath):
kitcat_root = get_project_root(kitcat_paths)
abx_data = combine_yaml(collect_yaml_files(filepath,
'abx', root=kitcat_root))
abx_data = combine_yaml([ABX_YAML])['abx']
abx_data.update(combine_yaml(collect_yaml_files(filepath,
'abx', root=kitcat_root)))
return kitcat_root, kitcat_data, abx_data

View File

@ -58,6 +58,11 @@ Demo:
import os, re, copy, string, collections
import yaml
DEFAULT_YAML = {}
with open(os.path.join(os.path.dirname(__file__), 'abx.yaml')) as def_yaml_file:
DEFAULT_YAML.update(yaml.safe_load(def_yaml_file))
TESTPATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'testdata', 'myproject', 'Episodes', 'A.001-Pilot', 'Seq', 'LP-LastPoint', 'A.001-LP-1-BeginningOfEnd-anim.txt'))
from . import accumulate
@ -66,6 +71,53 @@ from .accumulate import RecursiveDict
wordre = re.compile(r'([A-Z][a-z]+|[a-z]+|[0-9]+|[A-Z][A-Z]+)')
class Enum(dict):
def __init__(self, *options):
for i, option in enumerate(options):
if isinstance(option, list) or isinstance(option, tuple):
name = option[0]
self[i] = tuple(option)
else:
name = str(option)
self[i] = (option, option, option)
self[name] = i
if name not in ('name', 'number', 'options'):
setattr(self, name, i)
@property
def options(self):
"""
This gives the options in a Blender-friendly format, with
tuples of three strings for initializing bpy.props.Enum().
If the Enum was initialized with strings, the options will
contain the same string three times. If initialized with
tuples of strings, they will be used unaltered.
"""
options = []
number_keys = sorted([k for k in self.keys() if type(k) is int])
return [self[i] for i in number_keys]
def name(self, n):
if type(n) is int:
return self[n][0]
elif type(n) is str:
return n
else:
return None
def number(self, n):
if type(n) is str:
return self[n]
elif type(n) is int:
return n
else:
return None
log_level = Enum('DEBUG', 'INFO', 'WARNING', 'ERROR')
NameParsers = {} # Parser registry
def registered_parser(parser):
@ -352,9 +404,12 @@ class Parser_ABX_Schema(object):
start = 0
for start, (schema, name) in enumerate(zip(self.schemas, namepath)):
field, r, s = self._parse_beginning(remainder, schema.delimiter)
if field.lower() == schema.format.format(name).lower():
score += 1.0
break
try:
if field.lower() == schema.format.format(name).lower():
score += 1.0
break
except ValueError:
print(' (365) field, format', field, schema.format)
possible += 1.0
@ -366,11 +421,14 @@ class Parser_ABX_Schema(object):
if not remainder: break
field, remainder, s = self._parse_beginning(remainder, schema.delimiter)
score += s
if ( type(field) == str and
field.lower() == schema.format.format(name).lower()):
fields[schema.rank]={'code':field}
fields['rank'] = schema.rank
score += 1.0
try:
if ( type(field) == str and
field.lower() == schema.format.format(name).lower()):
fields[schema.rank]={'code':field}
fields['rank'] = schema.rank
score += 1.0
except ValueError:
print(' (384) field, format', field, schema.format)
possible += 2.0
# Remaining fields are authoritative (doesn't affect score)
@ -387,6 +445,77 @@ class Parser_ABX_Schema(object):
fields['role'] = self.roles_by_filetype[fields['filetype']]
return score/possible, fields
@registered_parser
class Parser_ABX_Fallback(object):
"""
Highly-tolerant parser to fall back to if the others fail
or can't be used.
"""
name = 'abx_fallback'
filetypes = DEFAULT_YAML['definitions']['filetypes']
roles = DEFAULT_YAML['definitions']['roles']
roles_by_filetype = (
DEFAULT_YAML['definitions']['roles_by_filetype'])
main_sep_re = re.compile(r'\W+') # Any single non-word char
comment_sep_re = re.compile(r'[\W_][\W_]+|[~#$!=+&]+')
def __init__(self, **kwargs):
pass
def _parse_ending(self, filename, separator):
try:
remainder, suffix = filename.rsplit(separator, 1)
score = 1.0
except ValueError:
remainder = filename
suffix = None
score = 0.0
return (suffix, remainder, score)
def __call__(self, filename, namepath):
fields = {}
score = 1.0
possible = 4.5
split = filename.rsplit('.', 1)
if len(split)<2 or split[1] not in self.filetypes:
fields['filetype'] = None
remainder = filename
score += 1.0
else:
fields['filetype'] = split[1]
remainder = split[0]
comment_match = self.comment_sep_re.search(remainder)
if comment_match:
fields['comment'] = remainder[comment_match.end():]
remainder = remainder[:comment_match.start()]
else:
fields['comment'] = None
role = self.main_sep_re.split(remainder)[-1]
if role in self.roles:
fields['role'] = role
remainder = remainder[:-1-len(role)]
score += 1.0
else:
fields['role'] = None
# Implied role
if fields['filetype'] in self.roles_by_filetype:
fields['role'] = self.roles_by_filetype[fields['filetype']]
score += 1.0
words = self.main_sep_re.split(remainder)
fields['code'] = ''.join([w.capitalize() for w in words])
fields['title'] = remainder
return score/possible, fields
class RankNotFound(LookupError):
@ -574,8 +703,19 @@ class NameContext(object):
"""
def __init__(self, container, fields=None, namepath_segment=(), ):
self.schemas = []
self.clear()
if container or fields or namepath_segment:
self.update(container, fields, namepath_segment)
def clear(self):
self.fields = {}
self.schemas = ['project']
self.rank = 0
self.code = 'untitled'
self.container = None
self.namepath_segment = []
def update(self, container=None, fields=None, namepath_segment=()):
self.container = container
if namepath_segment:
@ -584,12 +724,9 @@ class NameContext(object):
self.namepath_segment = []
try:
#self.namepath = self.container.namepath
self.schemas = self.container.schemas
except AttributeError:
self.schemas = []
#self.namepath = []
try:
self.omit_ranks = self.container.omit_ranks
@ -606,12 +743,6 @@ class NameContext(object):
self.fields.update(fields)
elif isinstance(fields, str):
self.fields.update(yaml.safe_load(fields))
# if 'code' in self.fields:
# self.namepath.append(self.fields['code'])
#self.code = self.fields[self.rank]['code']
def update_fields(self, data):
self.fields.update(data)
@ -684,7 +815,7 @@ class NameContext(object):
return None
@rank.setter
def set_rank(self, rank):
def rank(self, rank):
self.fields['rank'] = rank
@property
@ -699,7 +830,7 @@ class NameContext(object):
return ''
@name.setter
def set_name(self, name):
def name(self, name):
self.fields['name'] = name
@property
@ -710,7 +841,7 @@ class NameContext(object):
return self.fields['code']
@code.setter
def code_setter(self, code):
def code(self, code):
if self.rank:
self.fields[self.rank] = {'code': code}
else:
@ -724,7 +855,7 @@ class NameContext(object):
return ''
@description.setter
def set_description(self, description):
def description(self, description):
self.fields['description'] = str(description)
def _get_name_components(self):
@ -797,6 +928,7 @@ class FileContext(NameContext):
# hierarchy = None
#schema = None
# IMMUTABLE DEFAULTS:
filepath = None
root = None
folders = ()
@ -820,97 +952,151 @@ class FileContext(NameContext):
Collect path context information from a given filepath.
(Searches the filesystem for context information).
"""
#self.clear()
self.notes = []
# First init the superclass NameContext
NameContext.__init__(self, None, {})
self.namepath_segment = []
# TODO:
# I need to specify what happens when the path isn't defined.
# (Like we might need to initialize later?)
self.clear()
self.clear_notes()
if path:
self.update(path)
def clear(self):
NameContext.clear(self)
# Identity
self.root = os.path.abspath(os.environ['HOME'])
self.render_root = os.path.join(self.root, 'Renders')
self.filetype = ''
self.role = ''
self.title = ''
self.comment = ''
# Containers
#self.notes = []
self.name_contexts = []
# Status / Settings
self.filepath = None
self.filename = None
self.file_exists = False
self.folder_exists = False
self.omit_ranks = {
'edit': 0,
'render': 0,
'filename': 0,
'scene': 0}
# Defaults
self.provided_data = RecursiveDict(DEFAULT_YAML)
self.abx_fields = DEFAULT_YAML['abx']
def clear_notes(self):
# We use this for logging, so it doesn't get cleared by the
# normal clear process.
self.notes = []
def update(self, path):
# Basic File Path Info
self.filepath = os.path.abspath(path)
self.filename = os.path.basename(path)
# Basic File Path Info
self.filepath = os.path.abspath(path)
self.filename = os.path.basename(path)
# Does the file path exist?
if os.path.exists(path):
self.file_exists = True
self.folder_exists = True
else:
self.file_exists = False
if os.path.exists(os.path.dirname(path)):
self.folder_exists = True
else:
self.folder_exists = False
# Does the file path exist?
# - Should we create it? / Are we creating it?
# We should add a default YAML file in the ABX software to guarantee
# necessary fields are in place, and to document the configuration for
# project developers.
# Data from YAML Files
self._collect_yaml_data()
# Did we find the YAML data for the project?
# Did we find the project root?
# TODO: Bug?
# Note that 'project_schema' might not be correct if overrides are given.
# As things are, I think it will simply append the overrides, and this
# may lead to odd results. We'd need to actively compress the list by
# overwriting according to rank
#
# - Should we create it? / Are we creating it?
# We should add a default YAML file in the ABX software to guarantee
# necessary fields are in place, and to document the configuration for
# project developers.
# Data from YAML Files
#self._collect_yaml_data()
self.provided_data = RecursiveDict(DEFAULT_YAML)
kitcat_root, kitcat_data, abx_data = accumulate.get_project_data(self.filepath)
self.root = kitcat_root
self.provided_data.update(kitcat_data)
path = os.path.abspath(os.path.normpath(self.filepath))
root = os.path.abspath(self.root)
self.folders = [os.path.basename(self.root)]
self.folders.extend(os.path.normpath(os.path.relpath(path, root)).split(os.sep)[:-1])
self.abx_fields = abx_data
# Did we find the YAML data for the project?
# Did we find the project root?
# TODO: Bug?
# Note that 'project_schema' might not be correct if overrides are given.
# As things are, I think it will simply append the overrides, and this
# may lead to odd results. We'd need to actively compress the list by
# overwriting according to rank
#
try:
self._load_schemas(self.provided_data['project_schema'])
self.namepath_segment = [d['code'] for d in self.provided_data['project_unit']]
self.code = self.namepath[-1]
# Was there a "project_schema" section?
# - if not, do we fall back to a default?
# Was there a "project_unit" section?
# - if not, can we construct what we need from project_root & folders?
# Is there a definitions section?
# Do we provide defaults?
try:
self.render_root = os.path.join(self.root,
self.provided_data['definitions']['render_root'])
except KeyError:
self.render_root = os.path.join(self.root, 'Renders')
self.omit_ranks = {}
try:
for key, val in self.provided_data['definitions']['omit_ranks'].items():
self.omit_ranks[key] = int(val)
except KeyError:
self.omit_ranks.update({
'edit': 0,
'render': 1,
'filename': 1,
'scene': 3})
# Data from Parsing the File Name
try:
self.parsers = [NameParsers[self.provided_data['definitions']['parser']](**self.schema['filenames'])]
except (TypeError, KeyError, IndexError):
self.parsers = [
#Parser_ABX_Episode(),
Parser_ABX_Schema(self.schemas, self.provided_data['definitions'])]
except:
print("Errors finding Name Path (is there a 'project_schema' or 'project_unit' defined?")
pass
# print("\n(899) filename = ", self.filename)
# if 'project_schema' in self.provided_data:
# print("(899) project_schema: ", self.provided_data['project_schema'])
# else:
# print("(899) project schema NOT DEFINED")
#
# print("(904) self.namepath_segment = ", self.namepath_segment)
self.parser_chosen, self.parser_score = self._parse_filename()
# Was there a "project_schema" section?
# - if not, do we fall back to a default?
# Was there a "project_unit" section?
# - if not, can we construct what we need from project_root & folders?
# Is there a definitions section?
# Do we provide defaults?
try:
self.render_root = os.path.join(self.root,
self.provided_data['definitions']['render_root'])
except KeyError:
self.render_root = os.path.join(self.root, 'Renders')
self.omit_ranks = {}
try:
for key, val in self.provided_data['definitions']['omit_ranks'].items():
self.omit_ranks[key] = int(val)
except KeyError:
self.omit_ranks.update({
'edit': 0,
'render': 1,
'filename': 1,
'scene': 3})
# Data from Parsing the File Name
try:
self.parsers = [NameParsers[self.provided_data['definitions']['parser']](**self.schema['filenames'])]
except (TypeError, KeyError, IndexError):
self.parsers = [
#Parser_ABX_Episode(),
Parser_ABX_Schema(self.schemas, self.provided_data['definitions'])]
self.filetype = self.fields['filetype']
self.role = self.fields['role']
self.title = self.fields['title']
self.comment = self.fields['comment']
# TODO:
# We don't currently consider the information from the folder names,
# though we could get some additional information this way
# Empty / default attributes
self.name_contexts = []
parser_chosen, parser_score = self._parse_filename()
self.log(log_level.INFO, "Parsed with %s, score: %d" %
(parser_chosen, parser_score))
# TODO:
# We don't currently consider the information from the folder names,
# though we could get some additional information this way
def __repr__(self):
@ -919,7 +1105,18 @@ class FileContext(NameContext):
s = s + str(self.code) + '(' + str(self.rank) + ')'
s = s + ')'
return s
def log(self, level, msg):
if type(level) is str:
level = log_level.index(level)
self.notes.append((level, msg))
def get_log_text(self, level=log_level.INFO):
level = log_level.number(level)
return '\n'.join([
': '.join((log_level.name(note[0]), note[1]))
for note in self.notes
if log_level.number(note[0]) >= level])
def _parse_filename(self):
"""
@ -928,6 +1125,7 @@ class FileContext(NameContext):
"""
fields = {}
best_score = 0.0
best_parser_name = None
for parser in self.parsers:
score, fielddata = parser(self.filename, self.namepath)
if score > best_score:
@ -946,18 +1144,51 @@ class FileContext(NameContext):
self.fields[key] = val
def _collect_yaml_data(self):
self.provided_data = RecursiveDict()
kitcat_root, kitcat_data, abx_data = accumulate.get_project_data(self.filepath)
self.root = kitcat_root
self.provided_data.update(kitcat_data)
path = os.path.abspath(os.path.normpath(self.filepath))
root = os.path.abspath(self.root)
self.folders = [os.path.basename(self.root)]
self.folders.extend(os.path.normpath(os.path.relpath(path, root)).split(os.sep)[:-1])
self.abx_fields = abx_data
# def _collect_yaml_data(self):
@property
def filetype(self):
if 'filetype' in self.fields:
return self.fields['filetype']
else:
return ''
@filetype.setter
def filetype(self, filetype):
self.fields['filetype'] = filetype
@property
def role(self):
if 'role' in self.fields:
return self.fields['role']
else:
return ''
@role.setter
def role(self, role):
self.fields['role'] = role
@property
def title(self):
if 'title' in self.fields:
return self.fields['title']
else:
return ''
@title.setter
def title(self, title):
self.fields['title'] = title
@property
def comment(self):
if 'comment' in self.fields:
return self.fields['comment']
else:
return ''
@comment.setter
def comment(self, comment):
self.fields['comment'] = comment
@classmethod
def deref_implications(cls, values, matchfields):
@ -993,12 +1224,9 @@ class FileContext(NameContext):
fields = {}
fields.update(self.fields)
namepath_segment = []
ranks = [s.rank for s in self.schemas]
i_rank = len(self.namepath)
namepath_segment = []
ranks = [s.rank for s in self.schemas]
i_rank = len(self.namepath)
old_rank = ranks[i_rank -1]
# The new rank will be the highest rank mentioned, or the

View File

@ -3,81 +3,201 @@
Blender Python code to set parameters based on render profiles.
"""
import bpy
import bpy, bpy.types, bpy.utils, bpy.props
from . import std_lunatics_ink
from abx import ink_paint
render_formats = {
# VERY simplified and limited list of formats from Blender that we need:
# <API 'format'>: (<bpy file format>, <filename extension>),
'PNG': ('PNG', 'png'),
'JPG': ('JPEG', 'jpg'),
'EXR': ('OPEN_EXR_MULTILAYER', 'exr'),
'AVI': ('AVI_JPEG', 'avi'),
'MKV': ('FFMPEG', 'mkv')
}
from . import file_context
def set_render_from_profile(scene, profile):
if 'engine' in profile:
if profile['engine'] == 'gl':
pass
elif profile['engine'] == 'bi':
scene.render.engine = 'BLENDER_RENDER'
elif profile['engine'] == 'cycles':
scene.render.engine = 'CYCLES'
elif profile['engine'] == 'bge':
scene.render.engine = 'BLENDER_GAME'
class RenderProfile(object):
render_formats = {
# VERY simplified and limited list of formats from Blender that we need:
# <API 'format'>: (<bpy file format>, <filename extension>),
'PNG': ('PNG', 'png'),
'JPG': ('JPEG', 'jpg'),
'EXR': ('OPEN_EXR_MULTILAYER', 'exr'),
'AVI': ('AVI_JPEG', 'avi'),
'MKV': ('FFMPEG', 'mkv')
}
engines = {
'bi': 'BLENDER_RENDER',
'BLENDER_RENDER': 'BLENDER_RENDER',
'BI': 'BLENDER_RENDER',
'cycles': 'CYCLES',
'CYCLES': 'CYCLES',
'bge': 'BLENDER_GAME',
'BLENDER_GAME': 'BLENDER_GAME',
'BGE': 'BLENDER_GAME',
'gl': None,
'GL': None
}
def __init__(self, fields):
# Note: Settings w/ value *None* are left unaltered
# That is, they remain whatever they were before
# If a setting isn't included in the fields, then
# the attribute will be *None*.
if 'engine' not in fields:
fields['engine'] = None
if 'fps' in profile:
scene.render.fps = profile['fps']
if 'fps_skip' in profile:
scene.frame_step = profile['fps_skip']
if 'format' in profile:
scene.render.image_settings.file_format = render_formats[profile['format']][0]
if 'freestyle' in profile:
scene.render.use_freestyle = profile['freestyle']
if 'antialias' in profile:
if profile['antialias']:
scene.render.use_antialiasing = True
if profile['antialias'] in (5,8,11,16):
scene.render.antialiasing_samples = str(profile['antialias'])
if fields['engine']=='gl':
self.viewport_render = True
self.engine = None
else:
scene.render.use_antialiasing = False
if 'motionblur' in profile:
if profile['motionblur']:
scene.render.use_motion_blur = True
if type(profile['motionblur'])==int:
scene.render.motion_blur_samples = profile['motionblur']
self.viewport_render = False
if fields['engine'] in self.engines:
self.engine = self.engines[fields['engine']]
else:
scene.render.use_motion_blur = False
# Use Lunatics naming scheme for render target:
if 'framedigits' in profile:
framedigits = profile['framedigits']
else:
framedigits = 5
self.engine = None
# Parameters which are stored as-is, without modification:
self.fps = 'fps' in fields and int(fields['fps']) or None
self.fps_skip = 'fps_skip' in fields and int(fields['fps_skip']) or None
self.fps_divisor = 'fps_divisor' in fields and float(fields['fps_divisor']) or None
self.rendersize = 'rendersize' in fields and int(fields['rendersize']) or None
self.compress = 'compress' in fields and int(fields['compress']) or None
if 'suffix' in profile:
suffix = profile['suffix']
else:
suffix = ''
self.format = 'format' in fields and str(fields['format']) or None
if 'format' in profile:
rdr_fmt = render_formats[profile['format']][0]
ext = render_formats[profile['format']][1]
else:
rdr_fmt = 'PNG'
ext = 'png'
path = std_lunatics_ink.LunaticsShot(scene).render_path(
suffix=suffix, framedigits=framedigits, ext=ext, rdr_fmt=rdr_fmt)
scene.render.filepath = path
self.freestyle = 'freestyle' in fields and bool(fields['freestyle']) or None
self.antialiasing_samples = None
self.use_antialiasing = None
if 'antialias' in fields:
if fields['antialias']:
self.use_antialiasing = True
if fields['antialias'] in (5,8,11,16):
self.antialiasing_samples = str(fields['antialias'])
else:
self.use_antialiasing = False
self.use_motion_blur = None
self.motion_blur_samples = None
if 'motionblur' in fields:
if fields['motionblur']:
self.use_motion_blur = True
if type(fields['motionblur'])==int:
self.motion_blur_samples = int(fields['motionblur'])
else:
self.use_motion_blur = False
if 'framedigits' in fields:
self.framedigits = fields['framedigits']
else:
self.framedigits = 5
if 'suffix' in fields:
self.suffix = fields['suffix']
else:
self.suffix = ''
def apply(self, scene):
"""
Apply the profile settings to the given scene.
"""
if self.engine: scene.render.engine = self.engine
if self.fps: scene.render.fps = self.fps
if self.fps_skip: scene.frame_step = self.fps_skip
if self.fps_divisor: scene.render.fps_base = self.fps_divisor
if self.rendersize: scene.render.resolution_percentage = self.rendersize
if self.compress: scene.render.image_settings.compression = self.compress
if self.format:
scene.render.image_settings.file_format = self.render_formats[self.format][0]
if self.freestyle: scene.render.use_freestyle = self.freestyle
if self.use_antialiasing:
scene.render.use_antialiasing = self.use_antialiasing
if self.antialiasing_samples:
scene.render.antialiasing_samples = self.antialiasing_samples
if self.use_motion_blur:
scene.render.use_motion_blur = self.use_motion_blur
if self.motion_blur_samples:
scene.render.motion_blur_samples = self.motion_blur_samples
if self.format:
# prefix = scene.name_context.render_path
# prefix = BlendfileContext.name_contexts[scene.name_context].render_path
prefix = 'path_to_render' # We actually need to get this from NameContext
if self.suffix:
scene.render.filepath = (prefix + '-' + self.suffix + '-' +
'f'+('#'*self.framedigits) + '.' +
self.render_formats[self.format][1])
# def set_render_from_profile(scene, profile):
# if 'engine' in profile:
# if profile['engine'] == 'gl':
# pass
# elif profile['engine'] == 'bi':
# scene.render.engine = 'BLENDER_RENDER'
# elif profile['engine'] == 'cycles':
# scene.render.engine = 'CYCLES'
# elif profile['engine'] == 'bge':
# scene.render.engine = 'BLENDER_GAME'
#
# if 'fps' in profile:
# scene.render.fps = profile['fps']
#
# if 'fps_skip' in profile:
# scene.frame_step = profile['fps_skip']
#
# if 'format' in profile:
# scene.render.image_settings.file_format = render_formats[profile['format']][0]
#
# if 'freestyle' in profile:
# scene.render.use_freestyle = profile['freestyle']
#
# if 'antialias' in profile:
# if profile['antialias']:
# scene.render.use_antialiasing = True
# if profile['antialias'] in (5,8,11,16):
# scene.render.antialiasing_samples = str(profile['antialias'])
# else:
# scene.render.use_antialiasing = False
#
# if 'motionblur' in profile:
# if profile['motionblur']:
# scene.render.use_motion_blur = True
# if type(profile['motionblur'])==int:
# scene.render.motion_blur_samples = profile['motionblur']
# else:
# scene.render.use_motion_blur = False
#
# # Use Lunatics naming scheme for render target:
# if 'framedigits' in profile:
# framedigits = profile['framedigits']
# else:
# framedigits = 5
#
# if 'suffix' in profile:
# suffix = profile['suffix']
# else:
# suffix = ''
#
# if 'format' in profile:
# rdr_fmt = render_formats[profile['format']][0]
# ext = render_formats[profile['format']][1]
# else:
# rdr_fmt = 'PNG'
# ext = 'png'
#
# path = ink_paint.LunaticsShot(scene).render_path(
# suffix=suffix, framedigits=framedigits, ext=ext, rdr_fmt=rdr_fmt)
#
# scene.render.filepath = path

BIN
pkg/abx-0.2.6a.zip Normal file

Binary file not shown.

39
pkg/abx/__init__.py Normal file
View File

@ -0,0 +1,39 @@
bl_info = {
"name": "ABX",
"author": "Terry Hancock / Lunatics.TV Project / Anansi Spaceworks",
"version": (0, 2, 6),
"blender": (2, 79, 0),
"location": "SpaceBar Search -> ABX",
"description": "Anansi Studio Extensions for Blender",
"warning": "",
"wiki_url": "",
"tracker_url": "",
"category": "Object",
}
blender_present = False
try:
# These are protected so we can read the add-on metadata from my
# management scripts, which run in the O/S standard Python 3
import bpy, bpy.utils, bpy.types
blender_present = True
except ImportError:
print("Blender Add-On 'ABX' requires the Blender Python environment to run.")
if blender_present:
from . import abx_ui
def register():
abx_ui.register()
#bpy.utils.register_module(__name__)
def unregister():
abx_ui.unregister()
#bpy.utils.unregister_module(__name__)
if __name__ == "__main__":
register()

113
pkg/abx/abx.yaml Normal file
View File

@ -0,0 +1,113 @@
# DEFAULT ABX SETTINGS
---
abx_default: True
project_unit: []
project_schema: []
definitions:
filetypes:
blend: "Blender File"
kdenlive: "Kdenlive Video Editor File"
mlt: "Kdenlive Video Mix Script"
svg: "Scalable Vector Graphics (Inkscape)"
kra: "Krita Graphic File"
xcf: "Gimp Graphic File"
png: "Portable Network Graphics (PNG) Image"
jpg: "Joint Photographic Experts Group (JPEG) Image"
aup: "Audacity Project"
ardour: "Ardour Project"