Replaced file_context with abx_context

Removed file_context, name_context, name_schema, and ranks modules
(Moved into KitCAT Project).

Wrote much simpler abx_context to replace essential features for ABX.
This commit is contained in:
filmfreedom-org 2021-07-08 16:05:20 -05:00
parent 5e24e06796
commit c70a87de9f
17 changed files with 185 additions and 2826 deletions

View File

@ -9,8 +9,8 @@ import subprocess, os
import abx
VERSION_PKG = ('a',)
VERSION = abx.bl_info['version'] + VERSION_PKG
#VERSION_PKG = ('a',)
VERSION = abx.bl_info['version'] + (abx.version_ext,)
#VERSION = (0,1,2,'a')

View File

@ -2,7 +2,7 @@
bl_info = {
"name": "ABX",
"author": "Terry Hancock / Lunatics.TV Project / Anansi Spaceworks",
"version": (0, 2, 6),
"version": (0, 2, 7),
"blender": (2, 79, 0),
"location": "SpaceBar Search -> ABX",
"description": "Anansi Studio Extensions for Blender",
@ -12,7 +12,9 @@ bl_info = {
"category": "Object",
}
version_ext = 'dev'
# Appended to version in package name only
# Can be used to differentiate builds or development packages.
blender_present = False
try:

103
abx/abx_context.py Normal file
View File

@ -0,0 +1,103 @@
# abx_context.py
"""
Simplified file-context information, used to determine the correct source
of ABX settings from 'abx.yaml' files in the project folders.
This is reduced from the earlier attempt to use the file_context system
which I've moved into KitCAT.
"""
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))
from . import accumulate
from .accumulate import RecursiveDict
class ABX_Context(object):
"""
BlendFile context information.
"""
filepath = None
root = None
folders = ()
filename = None
def __init__(self, path=None):
self.clear()
if path:
self.update(path)
def clear(self):
"""
Clear contents of ABX_Context object.
Nearly the same as reinitializing, but the notes
attribute is left alone, to preserve the log history.
"""
# Identity
self.root = os.path.abspath(os.environ['HOME'])
self.render_root = os.path.join(self.root, 'Renders')
self.role = ''
# Status / Settings
self.filepath = None
self.filename = None
self.filetype = 'blend'
self.file_exists = False
self.folder_exists = False
# Defaults
self.provided_data = RecursiveDict(DEFAULT_YAML, source='default')
self.abx_fields = DEFAULT_YAML['abx']
self.render_profiles = {} #RenderProfileMap()
def update(self, path):
"""
Update the FileContext based on a new file 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
# Data from YAML Files
#self._collect_yaml_data()
self.provided_data = RecursiveDict(DEFAULT_YAML, source='default')
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
self.render_profiles = self.abx_fields['render_profiles']
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')

View File

@ -28,7 +28,7 @@ import os
import bpy, bpy.utils, bpy.types, bpy.props
from bpy.app.handlers import persistent
from . import file_context
from . import abx_context
from . import copy_anim
from . import ink_paint
from . import render_profile
@ -124,18 +124,18 @@ def get_seq_ids(self, context):
class ProjectProperties(bpy.types.PropertyGroup):
"""
Properties of the scene (and file), based on project context information.
Properties of the scene (and file), used for naming.
"""
name_context_id = bpy.props.StringProperty(options={'HIDDEN', 'LIBRARY_EDITABLE'})
@property
def name_context(self):
if self.name_context_id in BlendFile.name_contexts:
return BlendFile.name_contexts[self.name_context_id]
else:
name_context = BlendFile.new_name_context()
self.name_context_id = str(id(name_context))
return name_context
# name_context_id = bpy.props.StringProperty(options={'HIDDEN', 'LIBRARY_EDITABLE'})
#
# @property
# def name_context(self):
# if self.name_context_id in BlendFile.name_contexts:
# return BlendFile.name_contexts[self.name_context_id]
# else:
# name_context = BlendFile.new_name_context()
# self.name_context_id = str(id(name_context))
# return name_context
render_folder = bpy.props.StringProperty(
name = 'Render Folder',
@ -367,16 +367,29 @@ class RenderProfilesPanel(bpy.types.Panel):
row = self.layout.row()
row.operator('render.render_profiles')
class reset_rigify_after_scale(bpy.types.Operator):
"""
Fix selected Rigify-type Rig after scaling.
Resets the "STRETCH TO" constraints on a Rigify rig after they've been
disturbed by rescaling
"""
bl_idname = 'object.reset_scaled_rigify'
bl_label = "Reset Rigify"
bl_options = {'UNDO'}
def invoke(self, context, event):
tgt_obs = [ob for ob in context.selected_objects if ob.type == 'ARMATURE']
for ob in tgt_obs:
copy_anim.reset_armature_stretch_constraints(ob)
return {'FINISHED'}
class copy_animation(bpy.types.Operator):
"""
Copy animation from active object to selected objects (select source last!).
Useful for fixing broken proxy rigs (create a new proxy, and used this tool
to copy all animation from the original -- avoids tedious/error-prone NLA work).
Can also migrate to a re-scaled rig.
Useful for fixing broken proxy rigs. Can also re-scale
"""
bl_idname = 'object.copy_anim'
bl_label = 'Copy Animation'
@ -441,7 +454,6 @@ class copy_animation_settings(bpy.types.PropertyGroup):
default = 1.0)
class CharacterPanel(bpy.types.Panel):
"""
Features for working with characters and armatures.
@ -463,9 +475,10 @@ class CharacterPanel(bpy.types.Panel):
layout.prop(settings, 'nla')
layout.prop(settings, 'rescale')
layout.prop(settings, 'scale_factor')
layout.separator()
layout.operator('object.reset_scaled_rigify')
class lunatics_compositing_settings(bpy.types.PropertyGroup):
"""
@ -533,7 +546,7 @@ class LunaticsPanel(bpy.types.Panel):
layout.prop(settings, 'sepsky', text="Separate Sky")
BlendFile = file_context.FileContext()
BlendFile = abx_context.ABX_Context()
class RenderProfileSettings(bpy.types.PropertyGroup):
"""
@ -548,7 +561,7 @@ class RenderProfileSettings(bpy.types.PropertyGroup):
@persistent
def update_handler(ctxt):
"""
Keeps FileContext up-to-date with Blender file loaded.
Keeps ABX_Context up-to-date with Blender file loaded.
"""
BlendFile.update(bpy.data.filepath)
@ -567,6 +580,8 @@ def register():
type=RenderProfileSettings)
bpy.utils.register_class(RenderProfilesOperator)
bpy.utils.register_class(RenderProfilesPanel)
bpy.utils.register_class(reset_rigify_after_scale)
bpy.utils.register_class(copy_animation)
bpy.utils.register_class(copy_animation_settings)
@ -583,6 +598,10 @@ def register():
bpy.app.handlers.scene_update_post.append(update_handler)
def unregister():
bpy.app.handlers.save_post.remove(update_handler)
bpy.app.handlers.load_post.remove(update_handler)
bpy.app.handlers.scene_update_post.remove(update_handler)
bpy.utils.unregister_class(LunaticsSceneProperties)
bpy.utils.unregister_class(LunaticsScenePanel)
@ -591,6 +610,8 @@ def unregister():
bpy.utils.unregister_class(RenderProfileSettings)
bpy.utils.unregister_class(RenderProfilesOperator)
bpy.utils.unregister_class(RenderProfilesPanel)
bpy.utils.unregister_class(reset_rigify_after_scale)
bpy.utils.unregister_class(copy_animation)
bpy.utils.unregister_class(copy_animation_settings)
@ -598,4 +619,6 @@ def unregister():
bpy.utils.unregister_class(lunatics_compositing_settings)
bpy.utils.unregister_class(lunatics_compositing)
bpy.utils.unregister_class(LunaticsPanel)
bpy.utils.unregister_class(LunaticsPanel)

View File

@ -1,593 +0,0 @@
# file_context.py
"""
Contextual metadata acquired from the file system, file name, directory structure, and
sidecar data files.
Data is acquired from file and directory names and also from yaml files in the tree.
The yaml files are loaded in increasing priority from metadata.yaml, abx.yaml, <dirname>.yaml.
They are also loaded from the top of the tree to the bottom, with the most local Values
overriding the top-level ones.
@author: Terry Hancock
@copyright: 2019 Anansi Spaceworks.
@license: GNU General Public License, version 2.0 or later. (Python code)
Creative Commons Attribution-ShareAlike, version 3.0 or later. (Website Templates).
@contact: digitante@gmail.com
"""
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
from .accumulate import RecursiveDict
from .enum import Enum
from .ranks import RankNotFound
from abx.parsers import NameParsers
log_level = Enum('DEBUG', 'INFO', 'WARNING', 'ERROR')
from .name_schema import FieldSchema
from .name_context import NameContext
#from .render_profile import RenderProfileMap
class FileContext(NameContext):
"""
Collected information about a file's storage location on disk.
Collects name and path information from a filepath, used to identify
the file's role in a project. In order to do this correctly, the
FileContext object needs a schema defined for the project, which
explains how to read and parse project file names, to determine what
unit, name, or role they might have in the project.
For this, you will need to have a <project>.yaml file which defines
the 'project_schema' (a list of dictionaries used to initialize a list
of FieldSchema objects). Examples of <project>.yaml are provided in the
'myproject.yaml' file in the test data in the source distribution of
ABX, and you can also see a "live" example in the "Lunatics!" project.
Subclass from NameContext, so please read more information there.
Attributes:
root (filepath):
The root directory of the project as an absolute operating system
filepath. This should be used for finding the root where it is
currently, not stored for permanent use, as it will be wrong if
the project is relocated.
render_root (filepath):
The root directory for rendering. We often have this symlinked to
a large drive to avoid congestion. Usually just <root>/Renders.
filetype (str):
Filetype code or extension for this file. Usually identifies what
sort of file it is and may imply how it is used in some cases.
role (str):
Explicit definition of file's role in the project, according to
roles specified in <project>.yaml. For a default, see 'abx.yaml'
in the ABX source code. Derived from the file name.
title (str):
Title derived from the filename.
The relationship between this and the NameContext title is unclear
at present -- probably we should be setting the NameContext.title
property from here (?)
comment (str):
Comment field from the filename. This is a free field generally
occurring after the role, using a special delimiter and meant to
be readable by humans. It may indicate an informal backup or
saved version of the file outside of the VCS, as opposed to
a main, VCS-tracked copy. Or it may indicate some variant version
of the file.
name_contexts (list[NameContext]):
A list of NameContext objects contained in this file, typically
one-per-scene in a Blender file.
filepath (str):
O/S and location dependent absolute path to the file.
filename (str):
Unaltered filename from disk.
file_exists (bool):
Does the file exist on disk (yet)?
This may be false if the filename has been determined inside
the application, but the file has not been written to disk yet.
folder_exists (bool):
Does the containing folder exist (yet)?
folders (list(str)):
List of folder names from the project root to the current file,
forming a relative path from the root to this file.
omit_ranks (dict[str:int]):
How many ranks are omitted from the beginning of filename
fields? (Implementation).
provided_data (RecursiveDict):
The pile of data from project YAML files. This is a special
dictionary object that does "deep updates" in which sub-dictionaries
and sub-lists are updated recursively rather than simply being
replaced at the top level. This allows the provided_data to
accumulate information as it looks up the project tree to the
project root. It is not recommended to directly access this data.
(Implementation)
abx_fields (RecursiveDict):
A pile of 'abx.yaml' file with directives affecting how ABX should
behave with this file. This can be used to set custom behavior in
different project units. For example, we use it to define different
render profiles for different project units.
notes (list(str)):
A primitive logging facility. This stores warning and information
messages about the discovery process to aid the production designer
in setting up the project correctly.
NOTE that the clear method does not clear the notes! There is a
separate clear_notes() method.
parsers (list):
A list of registered parser implementations for analyzing file
names. FileContext tries them all, and picks the parser which
reports the best score -- that is, parser score themselves on
how likely their parse is to be correct. So if a parser hits a
problem, it demerits its score, allowing another parser to take
over.
Currently there are only three parsers provided: a custom one,
originally written to be specific to "Lunatics!" episodes
('abx_episode', now obsolete?), a parser using the project_schema
system ('abx_schema', now the preferred choice), and a "dumb"
parser design to fallback on if no schema is provided, which reads
only the filetype and possible role, title, and comment fields,
guessing from common usage with no explicit schema
('abx_fallback').
This implementation could probably benefit from some more application of
computer science and artificial intelligence, but I've settled on a
"good enough" solution and the assumption that production designers would
probably rather just learn how to use the YAML schemas correctly, than
to try to second-guess a sloppy AI system.
As of v0.2.6, FileContext does NOT support getting any information
directly from the operating system path for the file (i.e. by reading
directory names), although this would seem to be a good idea.
Therefore, project units have to be specified by additional unit-level
YAML documents (these can be quite small), explicitly setting the
unit-level information for directories above the current object, and
by inference from the project schema and the filename (which on "Lunatics!"
conveys all the necessary information for shot files, but perhaps not
for library asset files).
"""
# IMMUTABLE DEFAULTS:
filepath = None
root = None
folders = ()
#ranks = ()
project_units = ()
filename = None
fields = None
#subunits = ()
code = '_'
# defaults = {
# 'filetype': None, 'role': None, 'hierarchy': None, 'project': None,
# 'series': None, 'episode': None, 'seq': None, 'block': None,
# 'camera': None, 'shot': None, 'title': None }
def __init__(self, path=None):
"""
Collect path context information from a given filepath.
(Searches the filesystem for context information).
"""
NameContext.__init__(self, None, {})
self.clear()
self.clear_notes()
if path:
self.update(path)
def clear(self):
"""
Clear the contents of the FileContext object.
Nearly the same as reinitializing, but the notes
attribute is left alone, to preserve the log history.
"""
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, source='default')
self.abx_fields = DEFAULT_YAML['abx']
self.render_profiles = {} #RenderProfileMap()
def clear_notes(self):
"""
Clear the log history in the notes attribute.
"""
# We use this for logging, so it doesn't get cleared by the
# normal clear process.
self.notes = []
def update(self, path):
"""
Update the FileContext based on a new file 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
# - 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, source='default')
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?
self.render_profiles = self.abx_fields['render_profiles']
#self.render_profiles = RenderProfileMap(self.abx_fields['render_profiles'])
# 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]
except:
print("Can't find Name Path. Missing <project>.yaml file?")
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)
# 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
if ( 'parser' in self.provided_data['definitions'] and
self.provided_data['definitions']['parser'] in NameParsers):
# If project has defined what parser it wants (and it is registered),
# then restrict to that parser:
parser_selection = [self.provided_data['definitions']['parser']]
else:
parser_selection = NameParsers.keys()
if 'parser_options' in self.provided_data['definitions']:
parser_options = self.provided_data['definitions']['parser_options']
else:
parser_options = {}
# TESTING:
# Previous code locked-in the schema parser, so I'm starting with that:
parser_selection = ['abx_schema']
self.parsers = [NameParsers[p](
schemas = self.schemas,
definitions = self.provided_data['definitions'],
**parser_options)
for p in parser_selection]
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):
s = '{0}(data='.format(self.__class__.__name__)
#s = s + super().__repr__()
s = s + str(self.code) + '(' + str(self.rank) + ')'
s = s + ')'
return s
def log(self, level, msg):
"""
Log a message to the notes attribute.
This is a simple facility for tracking issues with the production
source tree layout, schemas, and file contexts.
"""
if type(level) is str:
level = log_level.index(level)
self.notes.append((level, msg))
def get_log_text(self, level=log_level.INFO):
"""
Returns the notes attribute as a block of text.
"""
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):
"""
Try available fuzzy data parsers on the filename, and pick the one
that returns the best score.
"""
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:
fields = fielddata
best_parser_name = parser.name
best_score = score
self.fields.update(fields)
self._pull_up_last_rank_fields()
return best_parser_name, best_score
def _pull_up_last_rank_fields(self):
if ( 'rank' in self.fields and
self.fields['rank'] in self.fields and
isinstance(self.fields[self.fields['rank']], collections.Mapping) ):
for key, val in self.fields[self.fields['rank']].items():
self.fields[key] = val
# def _collect_yaml_data(self):
@property
def filetype(self):
"""
Filetype suffix for the file (usually identifies format).
"""
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):
"""
Role field from the filename, or guessed from filetype.
"""
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):
"""
Title field parsed from the file name.
"""
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):
"""
Comment field parsed from the filename.
Meant to be a human-readable extension to the filename, often used to
represent an informal version, date, or variation on the file.
"""
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):
"""
NOT USED: Interpret information from reading folder names.
"""
subvalues = {}
for key in values:
# TODO: is it safe to use type tests here instead of duck tests?
if type(values[key])==int and values[key] < len(matchfields):
subvalues[key]=matchfields[values[key]]
elif type(values[key]==dict):
subvalues[key]=cls.deref_implications(values[key], matchfields)
elif type(values[key]==list):
vals = []
for val in values[key]:
vals.append(cls.deref_implications(val, matchfields))
return subvalues
def get_path_implications(self, path):
"""
NOT USED: Extract information from folder names.
"""
data = {}
prefix = r'(?:.*/)?'
suffix = r'(?:/.*)?'
for implication in self.schema['path_implications']:
matched = re.compile(prefix + implication['match'] + suffix).match(path)
if matched and matched.groups:
data.update(self.deref_implications(implication['values'], matched.groups()))
return data
def new_name_context(self, rank=None, **kwargs):
"""
Get a NameContext object representing a portion of this file.
In Blender, generally in a 1:1 relationship with locally-defined
scenes.
"""
fields = {}
fields.update(self.fields)
namepath_segment = []
ranks = [s.rank for s in self.schemas]
i_rank = len(self.namepath)
if i_rank == 0:
old_rank = None
else:
old_rank = ranks[i_rank -1]
# The new rank will be the highest rank mentioned, or the
# explicitly requested rank or
# one rank past the namepath
#
new_rank = self.schemas[i_rank].rank
for schema in self.schemas[i_rank:]:
if schema.rank in kwargs:
fields[schema.rank] = {'code':kwargs[schema.rank]}
new_rank = schema.rank
namepath_segment.append(kwargs[schema.rank])
elif rank is not None:
namepath_segment.append(schema.default)
if ranks.index(schema.rank) <= ranks.index(rank):
new_rank = schema.rank
if old_rank:
delta_rank = ranks.index(new_rank) - ranks.index(old_rank)
else:
# I think in this case, it's as if the old_rank number is -1?
delta_rank = ranks.index(new_rank) + 1
# Truncate to the new rank:
namepath_segment = namepath_segment[:delta_rank]
fields['rank'] = new_rank
fields['code'] = namepath_segment[-1]
name_context = NameContext(self, fields,
namepath_segment=namepath_segment)
self.name_contexts[str(id(name_context))] = name_context
return name_context

View File

@ -1,337 +0,0 @@
# name_context.py
"""
NameContext defines the context of a particular named asset.
Examples include scenes in a Blender file, but could be applied to other things.
Also a base class for FileContext which applies to the whole file.
NameContext handles the unified needs of these types, and in particular,
hosts the methods used to generate names.
"""
import os, re
import yaml
from .name_schema import FieldSchema
class NameContext(object):
"""
Single naming context within the file (e.g. a Blender scene).
NameContext defines the characteristics of any particular project
unit (taxon) within the project. So, for example, it may represent
the context of an "Episode" or a "Sequence" or a "Shot".
Used in Blender, it will typically be used in two ways: one to represent
the entire file (as the base class for FileContext) and one to represent
a particular Blender scene within the file, which may represent a single
shot, multiple shots with the same camera, or perhaps just an element
of a compositing shot created from multiple Blender scenes.
Examples of all three uses occur in "Lunatics!" episode 1: the
"Press Conference" uses multicam workflow, with scenes for each camera
with multiple shots selected from the timeline, using the VSE; most scenes
are done 'single camera' on a shot-per-scene basis; but some shots use a
'Freestyle camera clipping' technique which puts the line render in a
separate (but linked) scene, while the final shot in the episode combines
three different Blender scenes in a 2D composite, to effect a smooth
transition to the titles.
Attributes:
container (NameContext):
The project unit that contains this one. One step
up the tree, but not necessarily the next step up
in rank (because there can be skipped ranks).
schemas (list(FieldSchema)):
The schema list as seen by this unit, taking into
account any schema overrides.
namepath_segment (list):
List of namepath codes defined in this object, not
including the container's namepath. (Implementation)
omit_ranks dict(str:int):
How many ranks to omit from the beginning in shortened
names for specific uses. (Implementation).
Probably a mistake this isn't in the FieldSchema instead.
fields (dict): The field values used to initialize the NameContext,
May include some details not defined in this attribution
API, and it includes the raw state of the 'name', 'code',
and 'title' fields, determining which are
authoritative -- i.e. fields which aren't specified are
left to 'float', being generated by the related ones.
Thus, name implies title, or title implies name. You
can have one or the other or both, but the other will be
generated from the provided one if it isn't specified.
(Implementation).
code (str|int|Enum):
Identification code for the unit (I replaced 'id' in earlier
versions, because 'id' is a reserved word in Python for Python
memory reference / pointer identity.
(R/W Property).
namepath (list):
List of codes for project units above this one.
(R/O Property, generated from namepath_segment and
container).
rank (int): Rank of this unit (same as in Schema).
(R/W Property, may affect other attributes).
name (str): Short name for the unit.
(R/W Property, may affect title).
title (str): Full title for the unit.
(R/W Property, may affect name).
designation (str):
Full designation for the unit, including all
the namepath elements, but no title.
fullname (str): The full designation, plus the current unit name.
shortname (str):Abbreviated designation, according to omit_ranks,
with name.
"""
def __init__(self, container, fields=None, namepath_segment=(), ):
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:
self.namepath_segment = namepath_segment
else:
self.namepath_segment = []
try:
self.schemas = self.container.schemas
except AttributeError:
self.schemas = []
try:
self.omit_ranks = self.container.omit_ranks
except AttributeError:
self.omit_ranks = {}
self.omit_ranks.update({
'edit': 0,
'render': 1,
'filename': 1,
'scene': 3})
if fields:
if isinstance(fields, dict):
self.fields.update(fields)
elif isinstance(fields, str):
self.fields.update(yaml.safe_load(fields))
def update_fields(self, data):
self.fields.update(data)
def _load_schemas(self, schemas, start=0):
"""
Load schemas from a list of schema dictionaries.
@schemas: list of dictionaries containing schema field data (see FieldSchema).
The data will typically be extracted from YAML, and is
expected to be a list of dictionaries, each of which defines
fields understood by the FieldSchema class, to instantiate
FieldSchema objects. The result is a linked chain of schemas from
the top of the project tree down.
@start: if a start value is given, the top of the existing schema
chain is kept, and the provided schemas starts under the rank of
the start level in the existing schema. This is what happens when
the schema is locally overridden at some point in the hierarchy.
"""
self.schemas = self.schemas[:start]
if self.schemas:
last = self.schemas[-1]
else:
last = None
for schema in schemas:
self.schemas.append(FieldSchema(last, schema['rank'], schema=schema))
#last = self.schemas[-1]
def _parse_words(self, wordtext):
words = []
groups = re.split(r'[\W_]', wordtext)
for group in groups:
if len(group)>1:
group = group[0].upper() + group[1:]
words.extend(re.findall(r'[A-Z][a-z]*', group))
elif len(group)==1:
words.append(group[0].upper())
else:
continue
return words
def _cap_words(self, words):
return ''.join(w.capitalize() for w in words)
def _underlower_words(self, words):
return '_'.join(w.lower() for w in words)
def _undercap_words(self, words):
return '_'.join(w.capitalize() for w in words)
def _spacecap_words(self, words):
return ' '.join(w.capitalize() for w in words)
def _compress_name(self, name):
return self._cap_words(self._parse_words(name))
@property
def namepath(self):
if self.container:
return self.container.namepath + self.namepath_segment
else:
return self.namepath_segment
@property
def rank(self):
if 'rank' in self.fields:
return self.fields['rank']
else:
return None
@rank.setter
def rank(self, rank):
self.fields['rank'] = rank
@property
def name(self):
if 'name' in self.fields:
return self.fields['name']
elif 'title' in self.fields:
return self._compress_name(self.fields['title'])
# elif 'code' in self.fields:
# return self.fields['code']
else:
return ''
@name.setter
def name(self, name):
self.fields['name'] = name
@property
def code(self):
if self.rank:
return self.fields[self.rank]['code']
else:
return self.fields['code']
@code.setter
def code(self, code):
if self.rank:
self.fields[self.rank] = {'code': code}
else:
self.fields['code'] = code
@property
def description(self):
if 'description' in self.fields:
return self.fields['description']
else:
return ''
@description.setter
def description(self, description):
self.fields['description'] = str(description)
def _get_name_components(self):
components = []
for code, schema in zip(self.namepath, self.schemas):
if code is None: continue
components.append(schema.format.format(code))
components.append(schema.delimiter)
return components[:-1]
@property
def fullname(self):
if self.name:
return (self.designation +
self.schemas[-1].delimiter +
self._compress_name(self.name) )
else:
return self.designation
@property
def designation(self):
return ''.join(self._get_name_components())
@property
def shortname(self):
namebase = self.omit_ranks['filename']*2
return (''.join(self._get_name_components()[namebase:]) +
self.schemas[-1].delimiter +
self._compress_name(self.name))
def get_scene_name(self, suffix=''):
"""
Create a name for the current scene, based on namepath.
Arguments:
suffix (str): Optional suffix code used to improve
identifications of scenes.
"""
namebase = self.omit_ranks['scene']*2
desig = ''.join(self._get_name_components()[namebase:])
if suffix:
return desig + ' ' + suffix
else:
return desig
def get_render_path(self, suffix='', framedigits=5, ext='png'):
"""
Create a render filepath, based on namepath and parameters.
Arguments:
suffix (str):
Optional unique code (usually for render profile).
framedigits (int):
How many digits to reserve for frame number.
ext (str):
Filetype extension for the render.
This is meant to be called by render_profile to combine namepath
based name with parameters for the specific render, to uniquely
idenfify movie or image-stream output.
"""
desig = ''.join(self._get_name_components()[self.omit_ranks['render']+1:])
if ext in ('avi', 'mov', 'mp4', 'mkv'):
if suffix:
path = os.path.join(self.render_root, suffix,
desig + '-' + suffix + '.' + ext)
else:
path = os.path.join(self.render_root, ext.upper(),
desig + '.' + ext)
else:
if suffix:
path = os.path.join(self.render_root,
suffix, desig,
desig + '-' + suffix + '-f' + '#'*framedigits + '.' + ext)
else:
path = os.path.join(self.render_root,
ext.upper(), desig,
desig + '-f' + '#'*framedigits + '.' + ext)
return path

View File

@ -1,286 +0,0 @@
# name_schema.py
"""
Object for managing schema directives from project YAML and applying them to parsing and mapping name fields.
"""
import string, collections
from .ranks import RankNotFound, Rank, Branch, Trunk
class FieldSchema(object):
"""
Represents a schema used for parsing and constructing a field in names.
We need naming information in various formats, based on knowledge about
the role of the Blender file and scene in the project. This object tracks
this information and returns correct names based on it via properties.
Note that FieldSchema is NOT an individual project unit name, but a defined
pattern for how names are treat at that level in the project. It is a class
of names, not a name itself. Thus "shot" has a schema, but is distinct from
"shot A" which is a particular "project unit". The job of the schema is
to tell us things like "shots in this project will be represented by
single capital letters".
See NameContext for the characteristics of a particular unit.
Attributes:
codetype (type): Type of code name used for this rank.
Usually it will be int, str, or Enum.
Pre-defined enumerations are available
for uppercase letters (_letters) and
lowercase letters (_lowercase) (Roman --
in principle, other alphabets could be added).
rank (Rank): Rank of hierarchy under project (which is 0). The
rank value increases as you go "down" the tree.
Sorry about that confusion.
ranks (Branch): List of named ranks known to schema (may include
both higher and lower ranks).
parent (|None):
Earlier rank to which this schema is attached.
format (str): Code for formatting with Python str.format() method.
Optional: format can also be specified with the
following settings, or left to default formatting.
pad (str): Padding character.
minlength (int): Minimum character length (0 means it may be empty).
maxlength (int): Maximum character length (0 means no limit).
words (bool): Treat name/title fields like a collection of words,
which can then be represented using "TitleCaps" or
"underscore_spacing", etc for identifier use.
delimiter (str): Field delimiter marking the end of this ranks'
code in designations. Note this is the delimiter
after this rank - the higher (lower value) rank
controls the delimiter used before it.
default: The default value for this rank. May be None,
in which case, the rank will be treated as unset
until a setting is made. The UI must provide a
means to restore the unset value. Having no values
set below a certain rank is how a NameContext's
rank is determined.
Note that the rank may go back to a lower value than the schema's
parent object in order to overwrite earlier schemas (for overriding a
particular branch in the project) or (compare this to the use of '..'
in operating system paths). Or it may skip a rank, indicating an
implied intermediate value, which will be treated as having a fixed
value. (I'm not certain I want that, but it would allow us to keep
rank numbers synchronized better in parallel hierarchies in a project).
Note that schemas can be overridden at any level in a project by
'project_schema' directives in unit YAML files, so it is possible to
change the schema behavior locally. By design, only lower levels in
the hierarchy (higher values of rank) can be affected by overrides.
This kind of use isn't fully developed yet, but the plan is to include
things like managing 'Library' assets with a very different structure
from shot files. This way, the project can split into 'Library' and
'Episode' forks with completely different schemas for each.
"""
# Defaults
_default_schema = {
'delimiter':'-',
'type': 'string',
'format':'{:s}',
'minlength':1, # Must be at least one character
'maxlength':0, # 0 means unlimited
'words': False, # If true, treat value as words and spaces
'pad': '0', # Left-padding character for fixed length
'default': None,
'rank': 'project',
'irank': 0,
'ranks': ('series', 'episode', 'sequence',
'block', 'camera', 'shot', 'element')
}
# Really this is more like a set than a dictionary right now, but I
# thought I might refactor to move the definitions into the dictionary:
_codetypes = {
'number':{},
'string':{},
'letter':{},
'lowercase':{},
}
_letter = tuple((A,A,A) for A in string.ascii_uppercase)
_lowercase = tuple((a,a,a) for a in string.ascii_lowercase)
rank = 'project'
irank = 0
default = None
#ranks = ('project',)
branch = Trunk
def __init__(self, parent=None, rank=None, schema=None, debug=False):
"""
Create a FieldSchema from schema data source.
FieldSchema is typically initialized based on data from YAML files
within the project. This allows us to avoid encoding project structure
into ABX, leaving how units are named up to the production designer.
If you want our suggestions, you can look at the "Lunatics!" project's
'lunatics.yaml' file, or the 'myproject.yaml' file in the ABX source
distribution.
Arguments:
parent (FieldSchema):
The level in the schema hierarchy above this one.
Should be None if this is the top.
rank (int): The rank of this schema to be created.
schema (dict): Data defining the schema, typically loaded from a
YAML file in the project.
debug (bool): Used only for testing. Turns on some verbose output
about internal implementation.
Note that the 'rank' is specified because it may NOT be sequential from
the parent schema.
"""
# Three types of schema data:
# Make sure schema is a copy -- no side effects!
if not schema:
schema = {}
else:
s = {}
s.update(schema)
schema = s
if not rank and 'rank' in schema:
rank = schema['rank']
# Stepped down in rank from parent:
self.parent = parent
if parent and rank:
# Check rank is defined in parent ranks and use that