Skip to content

Commit

Permalink
Adds support for nesting sequences and asking questions about nested …
Browse files Browse the repository at this point in the history
…items and sequences. (#23)

* Hide the .children attribute on compositon.

- This allows us to intercept insertion to Compositions in
the future for de-duplications.

* Working commit, adding unit test that calls copy.

* Add copy and deepcopy methods to item.

* Implement __copy__ and __deepcopy__ for core.Item.

This enables the python `copy` module to function on core.Items.

* Add copy functions to SerializeableObject.

* Add unit test for type preservation on copy.

* Promote copy/deepcopy to serializeable_object.

* Implement copy/deepcopy for composition.

* Add range_of_child.

* Autopep8 pass.

* Fix bug in range algorithm.

* Add documentation to Clip and Item.

- Remove dead code
- Small style tweak

* Clarify comments regarding copy policy.

* Refactor out code to find children to composition.

* PEP8 pass.

* Refactor to set_parent on Item.

This makes more sense since it manipulates the hidden ._parent pointer.

* Review notes:

- Remove NoSuchChildAtIndex
- clean up variable names

* Add error suffix to exceptions.

* Add unit test to clarify duration.

* Refactor range_of_child up to composition

* Add range_of_child to Timeline

* Add trimmed_range_of_child to Stack and composition.

* trimmed_range_of_child_at_index for Sequence

* Add transformed_time and reference_space to range.

* PEP8 pass.

* Remove vestigal _is_parent_of.

* set_parent -> _set_parent

* Clean up documentation on [trimmed_]range_of_child.

* Added TimeRange.contains(RationalTime) for clarity

* Clip needs to override trimmed_range so it can fall back to the media_reference's available_range.

* Filler is not visible. All other Items are.

* You can ask a Sequence for the child at a given time.

* An empty Stack has duration 0, instead of None.

* Exposed a public parent() getter on Item.

* Removed unnecessary arguments when making a 0 RationalTime.

* Compositions can determine which children overlap a given time.

* In a nested Composition, you can ask for the top clip at a given time.

* Lets test how nesting works in some more than trivial cases.

* Fixing bugs in transformed_time().

* Small pythonic cleanup pass.

- Switch to using enumerate in children_at_time
- Switch to using self-iteration rather than while loop in
top_clip_at_time

* More algorithmic simplification.

* Review Notes

* Remove unused exception.

* Reuse existing implementation when possible.

* Review notes

* Setitem shouldn't change length.

* Removing unused, untested functions.

* Clean up contain method and add tests.

* Update shot_detect method.

* Removing dead code, post review.

* Visible should be a staticmethod, not a property.

* no reason for the extra raise

* PEP8 pass.
  • Loading branch information
ssteinbach authored and jminor committed Nov 8, 2016
1 parent 863ae5e commit e1fd7eb
Show file tree
Hide file tree
Showing 20 changed files with 1,611 additions and 143 deletions.
5 changes: 2 additions & 3 deletions examples/shot_detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,15 +126,14 @@ def _timeline_with_breaks(name, full_path, dryrun=False):
start_time,
end_time
)
clip.transform = None

available_range = _media_start_end_of(full_path)

clip.media_reference = otio.media_reference.External(
target_url="file://" + full_path,
available_range=available_range
)
track.children.append(clip)
track.append(clip)

playhead = end_time
shot_index += 1
Expand Down Expand Up @@ -218,7 +217,7 @@ def main():
otio.adapters.write_to_file(new_tl, otio_filename)
print "SAVED: {} with {} clips.".format(
otio_filename,
len(new_tl.tracks[0].children)
len(new_tl.tracks[0])
)

if __name__ == '__main__':
Expand Down
2 changes: 1 addition & 1 deletion opentimelineio/adapters/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def module(self):

def _execute_function(self, func_name, **kwargs):
if not hasattr(self.module(), func_name):
raise exceptions.AdapterDoesntSupportFunction(
raise exceptions.AdapterDoesntSupportFunctionError(
"Sorry, {} doesn't support {}.".format(self.name, func_name)
)
return (getattr(self.module(), func_name)(**kwargs))
Expand Down
8 changes: 4 additions & 4 deletions opentimelineio/adapters/cmx_3600.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ def __init__(self, edl_string):
self._parse_edl(edl_string)

self.timeline.tracks.append(self.V)
if self.A1.children:
if self.A1:
self.timeline.tracks.append(self.A1)
if self.A2.children:
if self.A2:
self.timeline.tracks.append(self.A2)

def _add_clip(self, line, comments):
Expand All @@ -63,7 +63,7 @@ def _add_clip(self, line, comments):

try:
for channel in channel_map[clip_handler.channel_code]:
getattr(self, channel).children.append(clip_handler.clip)
getattr(self, channel).append(clip_handler.clip)
except KeyError as e:
raise RuntimeError('unknown channel code {0}'.format(e))

Expand Down Expand Up @@ -311,7 +311,7 @@ def write_to_string(input_otio):
edit_number = 1

track = input_otio.tracks[0]
for i, clip in enumerate(track.children):
for i, clip in enumerate(track):
source_tc_in = otio.opentime.to_timecode(clip.source_range.start_time)
source_tc_out = otio.opentime.to_timecode(clip.source_range.end_time())

Expand Down
249 changes: 235 additions & 14 deletions opentimelineio/core/composition.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"""
Composition Stack/Sequence Implementation
Composition base class. An object that contains `Items`:
- Sequences, Stacks (children of Composition)
- Clips
- Filler
"""

import collections
Expand All @@ -11,6 +14,11 @@
item
)

from .. import (
opentime,
exceptions
)


@type_registry.register_type
class Composition(item.Item, collections.MutableSequence):
Expand All @@ -33,12 +41,13 @@ def __init__(
)
collections.MutableSequence.__init__(self)

if children is None:
self.children = []
else:
self.children = children
self._children = []
if children:
# cannot simply set ._children to children since __setitem__ runs
# extra logic (assigning ._parent pointers).
self.extend(children)

children = serializeable_object.serializeable_field("children")
_children = serializeable_object.serializeable_field("children", list)

@property
def composition_kind(self):
Expand All @@ -48,7 +57,7 @@ def __str__(self):
return "{}({}, {}, {}, {})".format(
self._composition_kind,
str(self.name),
str(self.children),
str(self._children),
str(self.source_range),
str(self.metadata)
)
Expand All @@ -64,7 +73,7 @@ def __repr__(self):
self._modname,
self._composition_kind,
repr(self.name),
repr(self.children),
repr(self._children),
repr(self.source_range),
repr(self.metadata)
)
Expand All @@ -75,7 +84,7 @@ def __repr__(self):
def each_clip(self, search_range=None):
return itertools.chain.from_iterable(
(
c.each_clip(search_range) for i, c in enumerate(self.children)
c.each_clip(search_range) for i, c in enumerate(self._children)
if search_range is None or (
self.range_of_child_at_index(i).overlaps(search_range)
)
Expand All @@ -85,19 +94,231 @@ def each_clip(self, search_range=None):
def range_of_child_at_index(self, index):
raise NotImplementedError

def trimmed_range_of_child_at_index(self, index):
raise NotImplementedError

def __copy__(self):
result = super(Composition, self).__copy__()

# Children are *not* copied with a shallow copy since the meaning is
# ambiguous - they have a parent pointer which would need to be flipped
# or they would need to be copied, which implies a deepcopy().
#
# This follows from the python documentation on copy/deepcopy:
# https://docs.python.org/2/library/copy.html
#
# """
# - A shallow copy constructs a new compound object and then (to the
# extent possible) inserts references into it to the objects found in
# the original.
# - A deep copy constructs a new compound object and then, recursively,
# inserts copies into it of the objects found in the original.
# """
result._children = []

return result

def __deepcopy__(self, md):
result = super(Composition, self).__deepcopy__(md)

# deepcopy should have already copied the children, so only parent
# pointers need to be updated.
[c._set_parent(result) for c in result._children]

return result

def _path_to_child(self, child):
if not isinstance(child, item.Item):
raise TypeError(
"An object child of 'Item' is required, not type '{}'"
"".format(type(child))
)

current = child
parents = []

while(current is not self):
try:
current = current._parent
except AttributeError:
raise exceptions.NotAChildError(
"Item '{}' is not a child of '{}'."
"".format(child, self)
)

parents.append(current)

return parents

def range_of_child(self, child, reference_space=None):
"""
Return range of the child in reference_space coordinates, before the
self.source_range.
For example,
| [-----] | seq
[-----------------] Clip A
If ClipA has duration 17, and seq has source_range: 5, duration 15,
seq.range_of_child(Clip A) will return (0, 17)
ignoring the source range of seq.
To get the range of the child with the source_range applied, use the
trimmed_range_of_child() method.
"""

if not reference_space:
reference_space = self

parents = self._path_to_child(child)

result_range = child.source_range

current = child
result_range = None

for parent in parents:
index = parent.index(current)
parent_range = parent.range_of_child_at_index(index)

if not result_range:
result_range = parent_range
current = parent
continue

result_range.start_time = (
result_range.start_time +
parent_range.start_time
)
result_range.duration = result_range.duration
current = parent

if reference_space is not self:
result_range = self.transformed_time_range(
result_range,
reference_space
)

return result_range

def children_at_time(self, t):
""" Which children overlap time t? """

result = []
for index, child in enumerate(self):
if self.range_of_child_at_index(index).contains(t):
result.append(child)

return result

def top_clip_at_time(self, t):
for child in self.children_at_time(t):
if isinstance(child, Composition):
return child.top_clip_at_time(self.transformed_time(t, child))
elif not child.visible():
continue
else:
return child

return None

def trimmed_range_of_child(self, child, reference_space=None):
"""
Return range of the child in reference_space coordinates, after the
self.source_range is applied.
For example,
| [-----] | seq
[-----------------] Clip A
If ClipA has duration 17, and seq has source_range: 5, duration 10,
seq.trimmed_range_of_child(Clip A) will return (5, 10)
Which is trimming the range according to the source_range of seq.
To get the range of the child without the source_range applied, use the
range_of_child() method.
Another example:
| [-----] | seq source range starts on frame 4 and goes to frame 8
[ClipA][ClipB] (each 6 frames long)
seq.range_of_child(CLipA):
0, duration 6
seq.trimmed_range_of_child(ClipA):
4, duration 2
"""

if not reference_space:
reference_space = self

if not reference_space == self:
raise NotImplementedError

parents = self._path_to_child(child)

result_range = child.source_range

current = child
result_range = None

for parent in parents:
index = parent.index(current)
parent_range = parent.trimmed_range_of_child_at_index(index)

if not result_range:
result_range = parent_range
current = parent
continue

result_range.start_time = (
result_range.start_time +
parent_range.start_time
)
result_range.duration = result_range.duration
current = parent

if not self.source_range:
return result_range

new_start_time = max(
self.source_range.start_time,
result_range.start_time
)

# trimmed out
if new_start_time >= result_range.end_time():
return None

# compute duration
new_duration = min(
result_range.end_time(),
self.source_range.end_time()
) - new_start_time

if new_duration.value < 0:
return None

return opentime.TimeRange(new_start_time, new_duration)

# @{ collections.MutableSequence implementation
def __getitem__(self, item):
return self.children[item]
return self._children[item]

def __setitem__(self, key, value):
self.children[key] = value
value._set_parent(self)
self._children[key] = value

def insert(self, key, value):
self.children.insert(key, value)
value._set_parent(self)
self._children.insert(key, value)

def __len__(self):
return len(self.children)
return len(self._children)

def __delitem__(self, item):
del self.children[item]
thing = self._children[item]
del self._children[item]
thing._set_parent(None)
# @}
Loading

0 comments on commit e1fd7eb

Please sign in to comment.