diff --git a/abx/abx_ui.py b/abx/abx_ui.py index 166365f..ce0adbb 100644 --- a/abx/abx_ui.py +++ b/abx/abx_ui.py @@ -41,7 +41,6 @@ from . import copy_anim from abx import ink_paint from . import render_profile - #configfile = os.path.join(os.path.dirname(__file__), 'config.yaml') #print("Configuration file path: ", os.path.abspath(configfile)) @@ -223,6 +222,17 @@ class LunaticsSceneProperties(bpy.types.PropertyGroup): NOTE: due to be replaced by 'ProjectProperties', using the schema data retrieved by file_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 + series_id = bpy.props.EnumProperty( items=[ ('S1', 'S1', 'Series One'), diff --git a/abx/file_context.py b/abx/file_context.py index 182e110..3edd646 100644 --- a/abx/file_context.py +++ b/abx/file_context.py @@ -1344,7 +1344,7 @@ class FileContext(NameContext): # Containers #self.notes = [] - self.name_contexts = [] + self.name_contexts = {} # Status / Settings self.filepath = None @@ -1636,8 +1636,11 @@ class FileContext(NameContext): namepath_segment = [] ranks = [s.rank for s in self.schemas] - i_rank = len(self.namepath) - old_rank = ranks[i_rank -1] + 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 @@ -1655,17 +1658,24 @@ class FileContext(NameContext): if ranks.index(schema.rank) <= ranks.index(rank): new_rank = schema.rank - delta_rank = ranks.index(new_rank) - ranks.index(old_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] - - self.name_contexts.append(NameContext(self, fields, - namepath_segment=namepath_segment)) - return self.name_contexts[-1] + + name_context = NameContext(self, fields, + namepath_segment=namepath_segment) + + self.name_contexts[str(id(name_context))] = name_context + + return name_context diff --git a/abx/ranks.py b/abx/ranks.py new file mode 100644 index 0000000..cf05003 --- /dev/null +++ b/abx/ranks.py @@ -0,0 +1,247 @@ +# ranks.py +""" +Branches and ranks objects. + +Objects for representing the ranks of a hierarchy, with the +possibility of branching at nodes with redefined ranks via +the 'project_schema' directives in project YAML files. +""" + +import numbers + +class Branch(object): + """ + Branch represents a ranking system in the tree of schemas. + + It takes the name of the project_unit where the ranking system + is overridden, and controls how all ranks defined within it + are interpreted. + """ + def __init__(self, parent, code, start, ranks): + self.parent = parent + self.code = str(code) + self.start = int(start) + + self._ranks = RankList(self,[]) + if parent: + for rank in parent.ranks: + if int(rank) < start: + self._ranks.append(rank) + + for num, name in enumerate(ranks): + rank = Rank(self, num + start, name) + self._ranks.append(rank) + + def __repr__(self): + ranklist = ', '.join([str(r) for r in self.ranks[1:]]) + if self.code: + code = self.code + else: + code = 'trunk' + return "" % (code, ranklist) + + def __contains__(self, other): + if isinstance(other, Rank) and other in self._ranks: + return True + else: + return False + + def rank(self, n): + """ + Coerce int or string to rank, if it matches a rank in this Branch. + """ + if isinstance(n, int) and 0 < n < len(self._ranks): + return self._ranks[n] + elif isinstance(n, str): + for rank in self._ranks: + if str(rank) == n: + return rank + else: + raise TypeError + + @property + def ranks(self): + # Read-only property. + return self._ranks + + + +class Rank(object): + """ + Ranks are named numbers indicating the position in a hierarchy. + + They can be incremented and decremented. The value 0 represents the top + rank, with increasing numbers indicating finer grades in taxonomic rank. + + They can have integers added to or subtracted from them, meaning to go + down or up in rank. They cannot be added to each other, though. Note that + higher and lower rank in the real world since of more or less scope is + inverted as a number -- the lower rank has a higher number. + + There are upper and lower bounds to rank, defined by the schema 'Branch'. + + Coercing a rank to an integer (int()) returns the numeric rank. + + Coercing a rank to a string (str()) returns the rank name. + + The representation includes the branch name and rank name. + + Ranks can be subtracted, returning an integer representing the difference + in rank. + """ + def __init__(self, branch, num, name): + self.num = num + self.name = name + self.branch = branch + + def __index__(self): + return self.num + + def __int__(self): + return self.num + + def __str__(self): + return self.name + + def __repr__(self): + return '<%s:%d-%s>' % (self.branch.code, self.num, self.name) + + def __hash__(self): + return hash((self.branch, self.num, self.name)) + + def __eq__(self, other): + if isinstance(other, Rank): + if hash(self) == hash(other): + return True + else: + return False + elif isinstance(other, str): + if other == self.name: + return True + else: + return False + elif isinstance(other, int): + if other == self.num: + return True + else: + return False + else: + return False + + def __gt__(self, other): + if isinstance(other, Rank): + if self.num > other.num: + return True + else: + return False + elif isinstance(other, int): + if self.num > other: + return True + else: + return False + else: + raise TypeError("Rank can't compare to %s" % type(other)) + + def __lt__(self, other): + if isinstance(other, Rank): + if self.num < other.num: + return True + else: + return False + elif isinstance(other, int): + if self.num < other: + return True + else: + return False + else: + raise TypeError("Rank can't compare to %s" % type(other)) + + def __ge__(self, other): + return (self > other or self == other) + + def __le__(self, other): + return (self < other or self == other) + + + def __add__(self, other): + if isinstance(other, int): + if (self.num + other) < len(self.branch.ranks): + return self.branch.ranks[self.num+other] + elif (self.num + other) < 1: + return trunk + else: + return None + else: + raise TypeError("Changes in rank must be integers.") + + def __radd__(self, other): + return self.__add__(other) + + def __sub__(self, other): + if isinstance(other, Rank): + return (self.num - other.num) + elif isinstance(other, int): + if 0 < (self.num - other) < len(self.branch.ranks): + return self.branch.ranks[self.num-other] + elif (self.num - other) < 1: + return trunk + elif (self.num - other) > len(self.branch.ranks): + return None + else: + raise TypeError("Rank subtraction not defined for %s" % type(other)) + + def __rsub__(self, other): + if isinstance(other, Rank): + return (other.num - self.num) + else: + raise TypeError("Rank subtraction not defined for %s" % type(other)) + + +class RankList(list): + """ + Convenience wrapper for a list of ranks, with simplified look-up. + + This allows indexes and slices on the ranks to use Rank objects and/or + string names in addition to integers for accessing the elements of the + list. + + The RankList also has to know what branch it is indexing, so it includes + a 'branch' attribute. + """ + def __init__(self, branch, ranks): + self.branch = branch + for rank in ranks: + self.append(rank) + + def __getitem__(self, rank): + if isinstance(rank, Rank): + i = int(rank) + elif isinstance(rank, str): + i = [r.name for r in self].index(rank) + elif isinstance(rank, int): + i = rank + elif isinstance(rank, slice): + if rank.start is None: + j = 0 + else: + j = self.__getitem__(rank.start) + + if rank.stop is None: + k = len(self) + else: + k = self.__getitem__(rank.stop) + + s = [] + for i in range(j,k): + s.append(super().__getitem__(i)) + + return s + else: + raise IndexError("Type %s not a valid index for RankList" % type(rank)) + return super().__getitem__(i) + + +# Define the trunk branch object +# This schema will make sense for any unaffiliated Blender document, +# even if it hasn't been saved as a file yet: +trunk = Branch(None, '', 0, ('', 'file', 'scene')) diff --git a/doc/Illus/ranks_branches.svg b/doc/Illus/ranks_branches.svg new file mode 100644 index 0000000..21e42ca --- /dev/null +++ b/doc/Illus/ranks_branches.svg @@ -0,0 +1,690 @@ + + + + + + + + image/svg+xml + + + + + + + "trunk" + -filescene + MyProject + -projectseriesepisodesequenceblockcamerashotelement + -projectlibrarydepartmentcategorysubcategory + Library + library.yaml + myproject.yaml + abx/abx.yaml + Episodes + A.001-Pilot + Seq + LP-LastPoint + A.001-LP-1-BeginningOfEnd-anim.blend + A + Ranks: + Ranks: + Ranks: + At any level in the hierarchy, a project unitmay define a new schema with a 'project_schema'key.This key cannot change the interpretation of ranksabove the current unit, but only the ones below it(and the current unit itself).This creates a branch of the rank/schematree, which takes the name of the projectunit where the branch starts.(And the first unit that is not inherited fromits parent Branch). + episodes.yaml + project_unit: from_folders: True + The Episodes directory is nota unit, but each of the folders under it is (theyare 'series' units, all sharing the same 'series' rankin the 'myproject' branch.Rather than using a separate YAML files for eachseries, this sample project uses a single file at thetop with 'project_unit' key that is used as a template.It is distinguished from a normal 'project_unit' keyby providing the 'from_folders' setting, and notcontaining a 'code', 'name', or 'title' setting.Instead, these values will be set by the directory namesunder the current directory (this also means all directoriesmust be project units at this level).This allows for a simple fanout without needing lots of YAML files. + The project folder defines a schema in a YAML file matching the project directory name.(you can also call the file 'project.yaml' or 'kitcat.yaml' if you prefer).Three keys are mandatory for this file:project_root: The 'project_root' key only has to exist. It can be as simple as having a value of True. But this is an ideal place to store basic metadata about your organization and this project.project_unit: The 'project_unit' key identifies the top rank of the project (the current directory). It should contain a bullet list with a single entry identifying the code ('myproject'), 'name', 'title', and other details.project_schema: The 'project_schema' key defines the default hierarchy of project assets, and creates the 'myproject' branch of ranks, overriding the default "trunk" branch. All project files are of rank 'project' or below (with each having a higher rank number). As defined here, the default ranking is organized for episode shot files. + Before any file is loaded (filepath is ''),the rank hierarchy is controlled solelyby the default schema in the ABXprogram folder.It sets only the 'trunk' (rank 0),'file' (the level of the Blender file)and 'scene' (the level of a scenein thefile) as ranks.These are imposed by the nature ofthe program, so we can safely usethose as defaults.Any additional structure needs to begiven by a YAML file in the projectdirectory. + + The name path for this file is derived almostentirely from the filename. Only the 'project'and 'series' ranks are specified above.ABX currently ignores the folder names abovethis, even though they could be used to givemore information (e.g. this sequence is called"LastPoint", but without YAML, this is left unspecified. + + + + + + diff --git a/tests/__pycache__/test_render_profile.cpython-35.pyc b/tests/__pycache__/test_render_profile.cpython-35.pyc index 28e84da..9bd25f4 100644 Binary files a/tests/__pycache__/test_render_profile.cpython-35.pyc and b/tests/__pycache__/test_render_profile.cpython-35.pyc differ diff --git a/tests/test_ranks.py b/tests/test_ranks.py new file mode 100644 index 0000000..cad342c --- /dev/null +++ b/tests/test_ranks.py @@ -0,0 +1,159 @@ +# test_ranks +""" +Test the ranks module, which implements the branch and rank semantics and math. +""" +import unittest, os + +import sys +print("__file__ = ", __file__) +sys.path.append(os.path.normpath(os.path.join(__file__, '..', '..'))) + +from abx import ranks + +class BranchTests(unittest.TestCase): + def test_trunk_branch(self): + t = ranks.trunk.rank('') + f = ranks.trunk.rank('file') + s = ranks.trunk.rank('scene') + self.assertEqual(repr(ranks.trunk), "") + self.assertIn(t, ranks.trunk) + self.assertIn(f, ranks.trunk) + self.assertIn(s, ranks.trunk) + + + def test_defining_branch(self): + b = ranks.Branch(ranks.trunk, 'myproject', 1, + ('project', 'series', 'episode', 'sequence', + 'block', 'shot', 'element')) + + self.assertEqual(len(b._ranks), 8) + +class RanksTests(unittest.TestCase): + def setUp(self): + self.b = ranks.Branch(ranks.trunk, 'myproject', 1, + ('project', 'series', 'episode', 'sequence', + 'block', 'shot', 'element')) + + def test_rank_knows_branch(self): + sh = self.b.rank('shot') + self.assertIs(sh.branch, self.b) + self.assertEqual(sh.num, 6) + + def test_rank_representation(self): + ep = self.b.rank('episode') + self.assertEqual(repr(ep), '') + self.assertEqual(int(ep), 3) + self.assertEqual(str(ep), 'episode') + + sq = self.b.rank(4) + self.assertEqual(repr(sq), '') + self.assertEqual(int(sq), 4) + self.assertEqual(str(sq), 'sequence') + + def test_rank_addition(self): + ep = self.b.rank('episode') + sq = self.b.rank(4) + + self.assertEqual(sq, ep + 1) + self.assertEqual(sq, 1 + ep) + + def test_rank_subtraction(self): + ep = self.b.rank('episode') + sq = self.b.rank(4) + + self.assertEqual(ep, sq - 1) + self.assertEqual(sq, ep - (-1)) + self.assertEqual(sq - ep, 1) + self.assertEqual(ep - sq, -1) + + def test_rank_increment_decrement(self): + ep = self.b.rank('episode') + sq = self.b.rank(4) + + r = ep + r += 1 + self.assertEqual(r, sq) + + r = sq + r -= 1 + self.assertEqual(r, ep) + + def test_rank_comparisons_direct(self): + sh = self.b.rank('shot') + se = self.b.rank('series') + s1 = self.b.rank(2) + + self.assertEqual(se, s1) + self.assertGreater(sh, se) + self.assertLess(se, sh) + + def test_rank_comparisons_compound(self): + sh = self.b.rank('shot') + se = self.b.rank('series') + s1 = self.b.rank(2) + + self.assertNotEqual(sh, se) + self.assertGreaterEqual(sh, se) + self.assertLessEqual(se, sh) + self.assertLessEqual(s1, se) + self.assertGreaterEqual(se, s1) + + def test_rank_too_high(self): + sh = self.b.rank('shot') + el = self.b.rank('element') + + r = sh + 1 + s = sh + 2 + t = sh + 3 + + self.assertEqual(r, el) + self.assertEqual(s, None) + self.assertEqual(t, None) + + def test_rank_too_low(self): + se = self.b.rank('series') + pr = self.b.rank('project') + + r = se - 1 # Normal - 'project' is one below 'series' + s = se - 2 # ? Should this be 'project' or 'trunk'/None? + t = se - 3 # "` " + + self.assertEqual(r, pr) + self.assertEqual(s, ranks.trunk) + self.assertEqual(t, ranks.trunk) + + + def test_rank_slices_from_branch(self): + ranks = self.b.ranks + self.assertEqual( + ranks[1:4], + ranks[self.b.rank('project'):self.b.rank('sequence')]) + + self.assertEqual( + ranks[:], + ranks) + + def test_ranklist_slice_access(self): + ranks = self.b.ranks + + self.assertEqual( + ranks[1:4], + ranks['project':'sequence']) + + self.assertEqual( + ranks[:'sequence'], + ranks[0:4]) + + self.assertEqual( + ranks[1:'shot'], + ranks['project':6]) + + self.assertEqual( + ranks[self.b.ranks['sequence']:7], + ranks['sequence':7]) + + self.assertEqual( + ranks.branch, + self.b) + + \ No newline at end of file diff --git a/tests/test_render_profile.py b/tests/test_render_profile.py index da9bb28..d7fd186 100644 --- a/tests/test_render_profile.py +++ b/tests/test_render_profile.py @@ -63,7 +63,7 @@ class TestRenderProfile_Implementation(unittest.TestCase): self.assertEqual(self.scene.render.resolution_percentage, 100) self.assertEqual(self.scene.render.image_settings.compression, 50) - + \ No newline at end of file