""" This module defines the various "meta intervals", which execute other intervals either in parallel or in a specified sequential order. """ __all__ = ['MetaInterval', 'Sequence', 'Parallel', 'ParallelEndTogether', 'Track'] from panda3d.core import * from panda3d.direct import * from direct.directnotify.DirectNotifyGlobal import * from .IntervalManager import ivalMgr from . import Interval from direct.task.Task import TaskManager #if __debug__: # import direct.showbase.PythonUtil as PythonUtil PREVIOUS_END = CMetaInterval.RSPreviousEnd PREVIOUS_START = CMetaInterval.RSPreviousBegin TRACK_START = CMetaInterval.RSLevelBegin class MetaInterval(CMetaInterval): # This is a Python-C++ hybrid class. MetaInterval is a Python # extension of the C++ class CMetaInterval, which adds some # Python-specific features (like list management). # This is the base class of Sequence, Parallel, and Track. notify = directNotify.newCategory("MetaInterval") SequenceNum = 1 def __init__(self, *ivals, **kw): #if __debug__: # self.debugInitTraceback = PythonUtil.StackTrace( # "create interval", 1, 10) name = None #if len(ivals) == 2 and isinstance(ivals[1], str): # # If the second parameter is a string, it's the name. # name = ivals[1] # ivals = ivals[0] #else: # Look for the name in the keyword params. if 'name' in kw: name = kw['name'] del kw['name'] # If the keyword "autoPause" or "autoFinish" is defined to # non-zero, it means the interval may be automatically paused # or finished when CIntervalManager::interrupt() is called. # This is generally called only on a catastrophic situation # (for instance, the connection to the server being lost) when # we have to exit right away; these keywords indicate # intervals that might not be cleaned up by their owners. autoPause = 0 autoFinish = 0 if 'autoPause' in kw: autoPause = kw['autoPause'] del kw['autoPause'] if 'autoFinish' in kw: autoFinish = kw['autoFinish'] del kw['autoFinish'] # A duration keyword specifies the duration the interval will # appear to have for the purposes of computing the start time # for subsequent intervals in a sequence or track. self.phonyDuration = -1 if 'duration' in kw: self.phonyDuration = kw['duration'] del kw['duration'] if kw: self.notify.error("Unexpected keyword parameters: %s" % (list(kw.keys()))) # We must allow the old style: Track([ival0, ival1, ...]) as # well as the new style: Track(ival0, ival1, ...) # Note: this breaks in the case of a Track with one tuple: # Track((0, ival0),). We could go through some effort to fix # this case, but for now I prefer just to document it as a # bug, since it will go away when we eventually remove support # for the old interface. #if len(ivals) == 1 and \ # (isinstance(ivals[0], tuple) or \ # isinstance(ivals[0], list)): # self.ivals = ivals[0] #else: self.ivals = ivals self.__ivalsDirty = 1 if name == None: name = self.__class__.__name__ + '-%d' if '%' in name: name = name % (self.SequenceNum) MetaInterval.SequenceNum += 1 CMetaInterval.__init__(self, name) self.__manager = ivalMgr self.setAutoPause(autoPause) self.setAutoFinish(autoFinish) self.pstats = None if __debug__ and TaskManager.taskTimerVerbose: self.pname = name.split('-', 1)[0] self.pstats = PStatCollector("App:Show code:ivalLoop:%s" % (self.pname)) self.pythonIvals = [] # If we are running in debug mode, we validate the intervals # in the list right away. There's no good reason to do this, # except that it makes it easier for the programmer to detect # when a MetaInterval is misdefined at creation time. assert self.validateComponents(self.ivals) # Functions to make the MetaInterval object act just like a Python # list of intervals: def append(self, ival): # Appends a single interval to the list so far. if isinstance(self.ivals, tuple): self.ivals = list(self.ivals) self.ivals.append(ival) self.__ivalsDirty = 1 assert self.validateComponent(ival) def extend(self, ivals): # Appends a list of intervals to the list so far. self += ivals def count(self, ival): # Returns the number of occurrences of the indicated interval. return self.ivals.count(ival) def index(self, ival): # Returns the position of the indicated interval within the list. return self.ivals.index(ival) def insert(self, index, ival): # Inserts the given interval into the middle of the list. if isinstance(self.ivals, tuple): self.ivals = list(self.ivals) self.ivals.insert(index, ival) self.__ivalsDirty = 1 assert self.validateComponent(ival) def pop(self, index = None): # Returns element index (or the last element) and removes it # from the list. if isinstance(self.ivals, tuple): self.ivals = list(self.ivals) self.__ivalsDirty = 1 if index == None: return self.ivals.pop() else: return self.ivals.pop(index) def remove(self, ival): # Removes the indicated interval from the list. if isinstance(self.ivals, tuple): self.ivals = list(self.ivals) self.ivals.remove(ival) self.__ivalsDirty = 1 def reverse(self): # Reverses the order of the intervals. if isinstance(self.ivals, tuple): self.ivals = list(self.ivals) self.ivals.reverse() self.__ivalsDirty = 1 def sort(self, cmpfunc = None): # Sorts the intervals. (?) if isinstance(self.ivals, tuple): self.ivals = list(self.ivals) self.__ivalsDirty = 1 if cmpfunc == None: self.ivals.sort() else: self.ivals.sort(cmpfunc) def __len__(self): return len(self.ivals) def __getitem__(self, index): return self.ivals[index] def __setitem__(self, index, value): if isinstance(self.ivals, tuple): self.ivals = list(self.ivals) self.ivals[index] = value self.__ivalsDirty = 1 assert self.validateComponent(value) def __delitem__(self, index): if isinstance(self.ivals, tuple): self.ivals = list(self.ivals) del self.ivals[index] self.__ivalsDirty = 1 def __getslice__(self, i, j): if isinstance(self.ivals, tuple): self.ivals = list(self.ivals) return self.__class__(self.ivals[i: j]) def __setslice__(self, i, j, s): if isinstance(self.ivals, tuple): self.ivals = list(self.ivals) self.ivals[i: j] = s self.__ivalsDirty = 1 assert self.validateComponents(s) def __delslice__(self, i, j): if isinstance(self.ivals, tuple): self.ivals = list(self.ivals) del self.ivals[i: j] self.__ivalsDirty = 1 def __iadd__(self, other): if isinstance(self.ivals, tuple): self.ivals = list(self.ivals) if isinstance(other, MetaInterval): assert self.__class__ == other.__class__ ivals = other.ivals else: ivals = list(other) self.ivals += ivals self.__ivalsDirty = 1 assert self.validateComponents(ivals) return self def __add__(self, other): copy = self[:] copy += other return copy # Functions to define sequence, parallel, and track behaviors: def addSequence(self, list, name, relTime, relTo, duration): # Adds the given list of intervals to the MetaInterval to be # played one after the other. self.pushLevel(name, relTime, relTo) for ival in list: self.addInterval(ival, 0.0, PREVIOUS_END) self.popLevel(duration) def addParallel(self, list, name, relTime, relTo, duration): # Adds the given list of intervals to the MetaInterval to be # played simultaneously; all will start at the same time. self.pushLevel(name, relTime, relTo) for ival in list: self.addInterval(ival, 0.0, TRACK_START) self.popLevel(duration) def addParallelEndTogether(self, list, name, relTime, relTo, duration): # Adds the given list of intervals to the MetaInterval to be # played simultaneously; all will end at the same time, but # the longest interval will be started first to achieve this. maxDuration = 0 for ival in list: maxDuration = max(maxDuration, ival.getDuration()) self.pushLevel(name, relTime, relTo) for ival in list: self.addInterval(ival, maxDuration - ival.getDuration(), TRACK_START) self.popLevel(duration) def addTrack(self, trackList, name, relTime, relTo, duration): # Adds a "track list". This is a list of tuples of the form: # # (, , # PREVIOUS_END | PREVIOUS_START | TRACK_START) # # where is a relative time, in seconds, for the # to start, relative to either the end of the # previous interval (PREVIOUS_END), the start of the previous # interval (PREVIOUS_START) or the start of the track list # (TRACK_START). If the relative code is omitted, the default # is TRACK_START. self.pushLevel(name, relTime, relTo) for tupleObj in trackList: if isinstance(tupleObj, tuple) or \ isinstance(tupleObj, list): relTime = tupleObj[0] ival = tupleObj[1] if len(tupleObj) >= 3: relTo = tupleObj[2] else: relTo = TRACK_START self.addInterval(ival, relTime, relTo) else: self.notify.error("Not a tuple in Track: %s" % (tupleObj,)) self.popLevel(duration) def addInterval(self, ival, relTime, relTo): # Adds the given interval to the MetaInterval. if isinstance(ival, CInterval): # It's a C++-style Interval, so add it directly. if getattr(ival, "inPython", 0): # Actually, it's been flagged to run in Python, even # though it's a C++ Interval. It's probably got some # Python functors that must be invoked at runtime to # define some of its parameters. Treat it as a Python # interval. index = len(self.pythonIvals) self.pythonIvals.append(ival) self.addExtIndex(index, ival.getName(), ival.getDuration(), ival.getOpenEnded(), relTime, relTo) elif isinstance(ival, MetaInterval): # It's another MetaInterval, so copy in its intervals # directly to this object. We could just store the # MetaInterval itself, which would work, but we get a # performance advantage by flattening out the deeply # nested hierarchy into a linear list within the root # CMetaInterval object. ival.applyIvals(self, relTime, relTo) else: # Nope, a perfectly ordinary C++ interval. Hooray! self.addCInterval(ival, relTime, relTo) elif isinstance(ival, Interval.Interval): # It's a Python-style Interval, so add it as an external. index = len(self.pythonIvals) self.pythonIvals.append(ival) if self.pstats: ival.pstats = PStatCollector(self.pstats, ival.pname) self.addExtIndex(index, ival.getName(), ival.getDuration(), ival.getOpenEnded(), relTime, relTo) else: self.notify.error("Not an Interval: %s" % (ival,)) # Functions to support automatic playback of MetaIntervals along # with all of their associated Python callbacks: def setManager(self, manager): self.__manager = manager CMetaInterval.setManager(self, manager) def getManager(self): return self.__manager def setT(self, t): self.__updateIvals() CMetaInterval.setT(self, t) def start(self, startT = 0.0, endT = -1.0, playRate = 1.0): self.__updateIvals() self.setupPlay(startT, endT, playRate, 0) self.__manager.addInterval(self) def loop(self, startT = 0.0, endT = -1.0, playRate = 1.0): self.__updateIvals() self.setupPlay(startT, endT, playRate, 1) self.__manager.addInterval(self) def pause(self): if self.getState() == CInterval.SStarted: self.privInterrupt() self.__manager.removeInterval(self) self.privPostEvent() return self.getT() def resume(self, startT = None): self.__updateIvals() if startT != None: self.setT(startT) self.setupResume() self.__manager.addInterval(self) def resumeUntil(self, endT): self.__updateIvals() self.setupResumeUntil(endT) self.__manager.addInterval(self) def finish(self): self.__updateIvals() state = self.getState() if state == CInterval.SInitial: self.privInstant() elif state != CInterval.SFinal: self.privFinalize() self.__manager.removeInterval(self) self.privPostEvent() def clearToInitial(self): # This is overloaded at the Python level to properly call # pause() at the Python level, then upcall to finish the job # at the C++ level. self.pause() CMetaInterval.clearToInitial(self) # Internal functions: def validateComponent(self, component): # This is called only in debug mode to verify that the # indicated component added to the MetaInterval is appropriate # to this type of MetaInterval. In most cases except Track, # this is the same as asking that the component is itself an # Interval. return isinstance(component, CInterval) or \ isinstance(component, Interval.Interval) def validateComponents(self, components): # This is called only in debug mode to verify that all the # components on the indicated list are appropriate to this # type of MetaInterval. for component in components: if not self.validateComponent(component): return 0 return 1 def __updateIvals(self): # The MetaInterval object does not create the C++ list of # Intervals immediately; rather, it stores a Python list of # Intervals that will be compiled into the C++ list the first # time it is needed. # This design allows us to avoid creation of the C++ list for # nested MetaInterval objects, instead copying all nested # MetaInterval hierarchy into the root CMetaInterval object, # for a performance benefit. # This function is called only on the root MetaInterval # object, when it is time to build the C++ list for itself. if self.__ivalsDirty: self.clearIntervals() self.applyIvals(self, 0, TRACK_START) self.__ivalsDirty = 0 def clearIntervals(self): # This overrides the function defined at the C++ level to # reset the inPython flag. Clearing out the intervals list # allows us to run entirely in C++ again, at least until a new # Python interval gets added. CMetaInterval.clearIntervals(self) self.inPython = 0 def applyIvals(self, meta, relTime, relTo): # Add the intervals listed in this object to the given # MetaInterval object at the C++ level. This will make the # other MetaInterval object ready to play the intervals. # This function should be overridden in a derived class to # change the intepretation of the intervals in this list. In # the case of a MetaInterval directly, this is valid only if # the list has only zero or one intervals. if len(self.ivals) == 0: pass elif len(self.ivals) == 1: meta.addInterval(self.ivals[0], relTime, relTo) else: self.notify.error("Cannot build list from MetaInterval directly.") def setPlayRate(self, playRate): """ Changes the play rate of the interval. If the interval is already started, this changes its speed on-the-fly. Note that since playRate is a parameter to start() and loop(), the next call to start() or loop() will reset this parameter. """ if self.isPlaying(): self.pause() CMetaInterval.setPlayRate(self, playRate) self.resume() else: CMetaInterval.setPlayRate(self, playRate) def __doPythonCallbacks(self): # This function invokes any Python-level Intervals that need # to be invoked at this point in time. It must be called # after any call to setT() or setFinalT() or stepPlay(), or # some such; basically any function that might invoke an # interval. The C++ base class will invoke whatever C++ # intervals it can, and then indicate the Python intervals # that must be invoked through this interface. ival = None try: while (self.isEventReady()): index = self.getEventIndex() t = self.getEventT() eventType = self.getEventType() self.popEvent() ival = self.pythonIvals[index] ival.privDoEvent(t, eventType) ival.privPostEvent() ival = None except: if ival != None: print("Exception occurred while processing %s of %s:" % (ival.getName(), self.getName())) else: print("Exception occurred while processing %s:" % (self.getName())) print(self) raise def privDoEvent(self, t, event): # This function overrides the C++ function to initialize the # intervals first if necessary. if self.pstats: self.pstats.start() self.__updateIvals() CMetaInterval.privDoEvent(self, t, event) if self.pstats: self.pstats.stop() def privPostEvent(self): if self.pstats: self.pstats.start() self.__doPythonCallbacks() CMetaInterval.privPostEvent(self) if self.pstats: self.pstats.stop() def setIntervalStartTime(self, *args, **kw): # This function overrides from the parent level to force it to # update the interval list first, if necessary. self.__updateIvals() # Once we have monkeyed with the interval timings, we'd better # run the whole thing as a monolithic Python interval, since # we can't extract the ivals list back out and append them # into a parent MetaInterval. self.inPython = 1 return CMetaInterval.setIntervalStartTime(self, *args, **kw) def getIntervalStartTime(self, *args, **kw): # This function overrides from the parent level to force it to # update the interval list first, if necessary. self.__updateIvals() return CMetaInterval.getIntervalStartTime(self, *args, **kw) def getDuration(self): # This function overrides from the parent level to force it to # update the interval list first, if necessary. self.__updateIvals() return CMetaInterval.getDuration(self) def __repr__(self, *args, **kw): # This function overrides from the parent level to force it to # update the interval list first, if necessary. self.__updateIvals() return CMetaInterval.__repr__(self, *args, **kw) def __str__(self, *args, **kw): # This function overrides from the parent level to force it to # update the interval list first, if necessary. self.__updateIvals() return CMetaInterval.__str__(self, *args, **kw) def timeline(self, out = None): # This function overrides from the parent level to force it to # update the interval list first, if necessary. self.__updateIvals() if out == None: out = ostream CMetaInterval.timeline(self, out) add_sequence = addSequence add_parallel = addParallel add_parallel_end_together = addParallelEndTogether add_track = addTrack add_interval = addInterval set_manager = setManager get_manager = getManager set_t = setT resume_until = resumeUntil clear_to_initial = clearToInitial clear_intervals = clearIntervals set_play_rate = setPlayRate priv_do_event = privDoEvent priv_post_event = privPostEvent set_interval_start_time = setIntervalStartTime get_interval_start_time = getIntervalStartTime get_duration = getDuration class Sequence(MetaInterval): def applyIvals(self, meta, relTime, relTo): meta.addSequence(self.ivals, self.getName(), relTime, relTo, self.phonyDuration) class Parallel(MetaInterval): def applyIvals(self, meta, relTime, relTo): meta.addParallel(self.ivals, self.getName(), relTime, relTo, self.phonyDuration) class ParallelEndTogether(MetaInterval): def applyIvals(self, meta, relTime, relTo): meta.addParallelEndTogether(self.ivals, self.getName(), relTime, relTo, self.phonyDuration) class Track(MetaInterval): def applyIvals(self, meta, relTime, relTo): meta.addTrack(self.ivals, self.getName(), relTime, relTo, self.phonyDuration) def validateComponent(self, tupleObj): # This is called only in debug mode to verify that the # indicated component added to the MetaInterval is appropriate # to this type of MetaInterval. In most cases except Track, # this is the same as asking that the component is itself an # Interval. if not (isinstance(tupleObj, tuple) or \ isinstance(tupleObj, list)): # It's not a tuple. return 0 relTime = tupleObj[0] ival = tupleObj[1] if len(tupleObj) >= 3: relTo = tupleObj[2] else: relTo = TRACK_START if not (isinstance(relTime, float) or \ isinstance(relTime, int)): # First parameter is not a number. return 0 if not MetaInterval.validateComponent(self, ival): # Second parameter is not an interval. return 0 if relTo != PREVIOUS_END and \ relTo != PREVIOUS_START and \ relTo != TRACK_START: # Third parameter is an invalid value. return 0 # Looks good. return 1