Source code for gillcup.clock

# Encoding: UTF-8
"""Gillcup's Clock Class

In Gillcup, animation means two things: running code at specified times,
and changing object properties with time.

You will notice that the preceding sentence mentions time quite a lot. But
what is this time?

You could determine time by looking at the computer's clock, but that would
only work with real-time animations. When you'd want to render a movie,
where each frame takes 2 seconds to draw and there are 25 frames per
second, you'd be stuck.
That's why Gillcup introduces a flexible source of time, the Clock, which
keeps track of time and schedules actions.

Time is measured in “time units”.
What a time unit means is entirely up to the application – it could be
seconds, movie/simulation frames, etc.
"""

from __future__ import unicode_literals, division, print_function

import collections
import weakref
import heapq

from gillcup.properties import AnimatedProperty

_HeapEntry = collections.namedtuple('EventHeapEntry', 'time index action')

# Next action index; used to keep FIFO ordering for actions scheduled
# for the same time
next_index = 0


[docs]class Clock(object): """Keeps track of time and schedules events. Attributes: .. attribute:: time The current time on the clock. Never assign to it directly; use :meth:`~gillcup.Clock.advance()` instead. """ def __init__(self): # Time on the clock self.time = 0 # Heap queue of scheduled actions self.events = [] # Update functions (see `schedule_update_function`) self.update_functions = WeakSet() # Recursion guard flag for advance() self.advancing = False # List of dependent clocks self._subclocks = set() speed = AnimatedProperty(1, docstring="""Speed of the clock. When calling update(), the interval is multiplied by this value. The speed is an AnimatedProperty. When changing, beware that it is only checked when advance() is called or when a scheduled action is run, so speed animations will be only approximate. For better accuracy, call :meth:`~gillcup.Clock.advance` with small *dt*, or schedule a periodic dummy action at small inervals. """) @property def _next_event(self): try: event = self.events[0] events = [(event.time - self.time, event.index, self, event)] except IndexError: events = [] for subclock in self._subclocks: event = subclock._next_event # pylint: disable=W0212 if event: remain, index, clock, event = event try: remain /= subclock.speed except ZeroDivisionError: # zero speed – events never happen pass else: events.append((remain, index, clock, event)) try: return min(events) except ValueError: return None
[docs] def advance(self, dt): """Call to advance the clock's time Steps the clock dt units to the future, pausing at times when actions are scheduled, and running them. Attempting to move to the past (dt<0) will raise an error. """ dt *= self.speed if dt < 0: raise ValueError('Moving backwards in time') if self.advancing: raise RuntimeError('Clock.advance called recursively') self.advancing = True try: while True: event = self._next_event if not event: break event_dt, _index, clock, event = event if event_dt > dt: break if dt: self._advance(event_dt) self._run_update_functions() dt -= event_dt _evt = heapq.heappop(clock.events) assert _evt is event and clock.time == event.time clock.time = event.time event.action() if dt: self._advance(dt) self._run_update_functions() finally: self.advancing = False
def _advance(self, dt): self.time += dt for subclock in self._subclocks: subclock._advance(dt * subclock.speed) # pylint: disable=W0212
[docs] def schedule(self, action, dt=0): """Schedule an action to be run "dt" time units from the current time Scheduling is stable: if two things are scheduled for the same time, they will be called in the order they were scheduled. Scheduling an action in the past (dt<0) will raise an error. If the scheduled callable has a “schedule_callback” method, it will be called with the clock and the time it's been scheduled at. """ global next_index if dt < 0: raise ValueError('Scheduling an action in the past') next_index += 1 scheduled_time = self.time + dt entry = _HeapEntry(scheduled_time, next_index, action) heapq.heappush(self.events, entry) try: schedule_callback = action.schedule_callback except AttributeError: pass else: schedule_callback(self, scheduled_time)
[docs] def schedule_update_function(self, function): """Schedule a function to be called every time the clock advances Then function will be called a lot, so it shouldn't be very expensive. Only a weak reference is made to the function, so the caller should ensure another reference to it is retained as long as it should be called. """ self.update_functions.add(function)
[docs] def unschedule_update_function(self, function): """Unschedule a function scheduled by `schedule_update_function` """ # Don't raise an error if the function is no longer there – we're # dealing with weakrefs. self.update_functions.discard(function)
def _run_update_functions(self): for update_function in list(self.update_functions): update_function() for subclock in self._subclocks: subclock._run_update_functions() # pylint: disable=W0212
[docs]class Subclock(Clock): """A Clock that advances in sync with another Clock A Subclock advances whenever its *parent* clock does. Its `speed` attribute specifies the relative speed relative to the parent clock. For example, if speed==2, the subclock will run twice as fast as its parent clock. Unlike clocks synchronized via actions or update functions, the actions scheduled on a parent Clock and all subclocks are run in the correct sequence, with all clocks at the correct times when each action is run. """ def __init__(self, parent, speed=1): super(Subclock, self).__init__() self.speed = speed parent._subclocks.add(self) # pylint: disable=W0212
try: # pragma: no cover WeakSet = weakref.WeakSet # pylint: disable=E1101 except AttributeError: # pragma: no cover class WeakSet(weakref.WeakKeyDictionary): """Stripped-down WeakSet implementation for Python 2.6 (only defines the methods we need) """ def add(self, item): """Add an item to the set""" self[item] = None def discard(self, item): """Remove an item from the set""" self.pop(item)