338 lines
12 KiB
Python
338 lines
12 KiB
Python
# 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
|