Python in TouchDesigner | Extensions | TouchDesigner

Rounding out some of our work here with Python is to look extensions. If you’ve been following along with other posts you’ve probably already looked over some extensions in this post. If you’re brand new to this idea, check out that example first.

Rather than re-inventing the wheel and setting up a completely new example, let’s instead look at our previous example of making a logger and see how that would be different with extensions as compared to a module on demand.

A warning for those following along at home, we’re now knee deep in Python territory, so what’ we’ll find here is less specific to TouchDesigner and more of a look at using Classes in Python.

In our previous example looking at modules we wrote several functions that we left in a text DAT. We used the mod class in TouchDesigner to treat this text DAT as a python module. That’s a neat trick, and for some applications and situations this might be the right approach to take for a given problem. If we’re working on a stand alone component that we want to use and re-use with a minimal amount of additional effort, then extensions might be a great solution for us to consider.

Wait, what are extensions?! Extensions are way that we can extend a custom component that we make in TouchDesigner. This largely makes several scripting processes much simpler and allows us to treat a component more like an autonomous object rather than a complex set of dependent objects. If you find yourself re-reading that last statement let’s take a moment to see if we can find an analogy to help understand this abstract concept.

When we talked about for loops we used a simple analogy about washing dishes. In that example we didn’t bother to really think deeply about the process of washing dishes – we didn’t think about how much detergent, how much water, water temperature, washing method, et cetera. If we were to approach this problem more programatically we’d probably consider making a whole class to deal with the process of washing dishes:

class Dishes():

    def __init__( self ):
        return

    def Full_cycle( self, list_of_dishes ):
        # what if we want to wash and then dry?

        self.Wash( list_of_dishes, water_temp )
        self.Dry( list_of_dishes )

        return

    def Wash( self, list_of_dishes, water_temp ):
        # what does washing really entail
        # all of that would go here

        number_of_dishes    = len( list_of_dishes )
        detergent_amt       = self.Set_soap( number_of_dishes )
        water_temp          = self.Set_water_temp( water_temp )

        return

    def Dry( self, list_of_dishes ):
        # what does washing really entail
        # all of that would go here

        number_of_dishes     = len( list_of_dishes )
        dry_time             = self.Set_dry( number_of_dishes )
        return

    def Set_soap( self, number_of_dishes ):
        return

    def Set_water_temp( water_temp ):
        if water_temp == 'hot':
            temp            = 98

        elif water_temp == 'medium':
            temp            = 80

        if water_temp == 'low':
            temp            = 60

        return temp

    def Set_dry( self, number of dishes ):
        return

Okay, so the above seems awfully silly… what’s going on here? In our silly example we can see that rather than one long function for Wash() or Dry(), we instead use several smaller helper functions in the process. Why do this? Well, for one thing it lets you debug your code much more easily. It also means that by separating out some of these elements into different processes we can fix a single part of our pipeline without having to do a complete refactor of the entire Wash() or Dry() methods. Why does that matter? Well, what if we decide that there’s a better way to determine how much soap to use. Rather than having to sort through a single long complex method for Wash() we can instead just look at Set_soap(). It makes unit testing easier, and allows us to replace or develop that method outside the context of the larger method. It also means that if we’re collaborating with other programmers we can divide up the process of writing methods.

What’s this self. business? self. allows us to call a method from inside another method in the same class. This is where extensions begin to really shine as opposed to using modules on demand. We an also use things like attributes andinheritance.

Okay, so how can we think about actually using this?

Let’s take a look at what this approach looks like in python:

import datetime

log_file            = op( 'text_log_file' )
log_path            = "example_extensions/log_files/log.txt"

full_text           = '''{now}

Current Year        | {year}
Current Month       | {month}
Current Day         | {day}
Current Hour        | {hour}
Current Minute      | {minute}
Current Second      | {second}
Current Microsecond | {microsecond}
'''

raw_date_time       = "On {month}-{day}-{year} at {hour}:{minute}:{second}"

verbose_log_message = '''============================
VERBOSE MESSAGE

{date_time}
----------------------------
operator            || {operator}
At Network Location || {path}

----------------------------
Logged
{message}
============================
'''

log_message         = '''----------------------------
{now}
----------------------------
{operator}
{path}
{message}
'''

class Ext_example():

    def __init__( self ):
        '''The init function.

        We're not doing anything with our init function in this example
        so we'll leave this empty.

        Notes
        ---------------
        ''' 
        return

    def Log_date( self ):
        year                        = datetime.datetime.now().year
        month                       = datetime.datetime.now().month
        day                         = datetime.datetime.now().day
        hour                        = datetime.datetime.now().hour
        minute                      = datetime.datetime.now().minute
        second                      = datetime.datetime.now().second

        updated_log_date            = log_date.format( 
                                                        month       = month,
                                                        day         = day,
                                                        year        = year,
                                                        hours       = hour,
                                                        minutes     = minute,
                                                        seconds     = second
                                                    )

        return updated_log_date

    def Log_date_time( self ):
        '''Create a formatted time stamp

        A look at how we might create a formatted time stamp to use with
        various logging applications.

        Arguments
        --------------- 
        None

        Returns
        ---------------
        formatted_text( str ) - a string time stamp

        Notes
        ---------------
        '''

        now         = datetime.datetime.now()
        year        = datetime.datetime.now().year
        month       = datetime.datetime.now().month
        day         = datetime.datetime.now().day
        hour        = datetime.datetime.now().hour
        minute      = datetime.datetime.now().minute
        second      = datetime.datetime.now().second
        microsecond = datetime.datetime.now().microsecond

        date_time   = raw_date_time.format( 
                                        month   = month,
                                        day     = day,
                                        year    = year,
                                        hour    = hour,
                                        minute  = minute,
                                        second  = second
                                        )

        return date_time

    def Log_message( self, operator, message, verbose=False, text_port_print=True, append_log=True ):
        '''Logging Method.

        A simple look at how you might start to think about building a logger for a TouchDesigner
        application. A logger is a great way to build out files with time stamped events. The
        more complex a project becomes, the more important it can become to have some means
        of logging the operations of your program. Here's a simple look at what that might
        look like.

        Arguments
        --------------- 
        operator( touch object ) - the touch object whose path you'd like incldued in the log message
        message( str ) - a message to include in the log
        verbose( bool ) - a toggle for verbose or compact messages
        text_port_print( bool ) - a toggle to print to the text port, or not
        append_log( bool ) - a toggle to append to the log file , or not

        Example
        --------------- 
        target_op       = op( 'constant1' )
        message         = "This operator needs attention"

        parent().Log_message( target_op, message )

        also

        parent().Log_message( target_op, message, verbose = True )

        Returns
        ---------------
        None

        Notes
        ---------------
        You'll notice that some arguments receive default values. This is so you don't have
        to include them in the call. This means that by default the message will be compact, 
        will print to the text port, and will append the log file.
        '''

        path        = operator.path
        op_name     = operator.name

        # logic tests for verbose or compact
        if verbose:
            message = verbose_log_message.format(
                                                    date_time   = self.Log_date_time(),
                                                    operator    = op_name,
                                                    path        = path,
                                                    message     = message
                                                )

        else:
            message = log_message.format(
                                            now                 = self.Log_date_time(),
                                            operator            = op_name,
                                            path                = path,
                                            message             = message
                                        )

        # logic tests for text_port_print
        if text_port_print:
            print( message )

        else:
            pass

        # log tests for appending log
        if append_log:
            log_file.write( '\n' + message )

        else:
            pass

        # save the log file to disk - external from the TouchDesigner project   
        self.Save_log()

        return

    def Save_log( self ):
        '''Saves log to disk.

        This helper function saves the log file to disk.

        Notes
        ---------------
        None
        '''

        op( log_file ).save( log_path )

        return

    def Clear_log( self ):
        '''Clears Log File.

        This helper function clears the text dat used to hold the log file.

        Notes
        ---------------
        None
        '''

        op( log_file ).clear()

        return

So getting started we set up a couple of variables that we were going to reuse several times. Next we declared our class, and then nested our methods inside of that class. You’ll notice that different from methods, we need to include the argument self in our method definitions:

# syntax for just a function
def Clear_log():
    return

# syntax for just a method in a class
def Clear_log( self ):
    return

We can also see how we’ve got several helper functions here to get us up and running – ways to add to the log, save the log to an external file, a way to clear the log. We can imagine in the future we might want to add another method that both clears the log, and saves an empty file, like a reset. Since we’ve already broken those functions into their own methods we could simply add this method like this:

def Reset_log():
    self.Clear_log()
    self.Save_log()
    return

Now it’s time to experiment.

Over the course of this series we’ve looked at lots of fundamental pieces of working with Python in TouchDesigner. Now it’s your turn to start playing and experimenting.

Happy programming!

Python in TouchDesigner | Modules | TouchDesigner

There are a number of ways that we might use modules on demand in TouchDesigner. Before we get too far along, however, we might first ask “what is a module on demand?”

According to the TouchDesigner wiki:

The MOD class provides access to Module On Demand object, 
which allows DATs to be dynamically imported as modules. 
It can be accessed with the mod object, found in the automatically 
imported td module. 

Alternatively, one can use the regular python statement: import. 
Use of the import statement is limited to modules in the search path, 
where as the mod format allows complete statements in one line, 
which is more useful for entering expressions. Also note that DAT modules 
cannot be organized into packages as regular file system based 
python modules can be.

What does this mean? It’s hard to sum up in just a single sentence, but the big thing to take away is that we can essentially use any text DAT to hold whole functions for us that we can then call whenever we want.

Let’s take a closer look at this process. We’ll start with some simple ideas, then work our way up to something a little more complicated.

First we turn things way down, and just think about storing variables. To be clear, we probably wouldn’t use this in a project, but it can be helpful for us when we’re trying to understand what exactly is going on here.

Let’s create a new text DAT and call it “text_variables”, inside let’s put the following text:

width       = 1280
height      = 720

budget      = 'small'

Using the mod class we can access these variables in other operators! To do this we’ll use the following syntax:

mod( 'text_variables' ).width
mod( 'text_variables' ).height
mod( 'text_variables' ).budget

Try adding a constant CHOP, and a text TOP to your network and using the expressions above to retrieve these values.

Next try printing these values:

print( mod( 'text_variables' ).width )
print( mod( 'text_variables' ).height )
print( mod( 'text_variables' ).budget )

So, it looks like we can access the contents of a module as a means of storing variables. That’s hip. Let’s take a moment and circle back to one of the other use cases that we’ve already seen for a module. More than just a single value, we can also put a whole dictionary in a module and then call it on demand. We’ve already done this in some of our previous examples, but we can take a quick look at that process again to make sure we understand.

Let’s create a new text DAT called “text_dictionary_as_module”, inside of this text DAT let’s define the following dictionary:

fruit = {
    "apple" : 10,
    "orange"    : 5,
    "kiwi"  : 16
}

Let’s first print the whole dictionary object:

print( mod( 'text_dictionary_as_module' ).fruit )

Alternatively, we can also access individual keys in the dictionary:

mod( 'text_dictionary_as_module' ).fruit[ 'apple' ]
mod( 'text_dictionary_as_module' ).fruit[ 'orange' ]
mod( 'text_dictionary_as_module' ).fruit[ 'kiwi' ]

What can you do with this?! Well, you might store your config file in a text DAT that you can call from a module on demand. You might use this to store configuration variables for your UI – colors, fonts, etc. ; you might decide to use this to configure some portion of a network, or to hold on to data that you want to recall later; or really any number of things.

Before we get too excited about storing variables in modules on demand, let’s look at an even more powerful feature that will help us better understand where they really start to shine.

Up next we’re going to look at writing a simple function that we can use as a module on demand. In addition to writing some simple little functions, we’re also going to embrace docstrings – a feature of the python language that makes documenting your work easier. Docstings allow us to leave behind some notes for our future selves, or other programmers. One of the single most powerful changes you can make to how you work is to document your code. It’s a difficult practice to establish, and can be frustrating to maintain – but it is hands down one of the most important changes you can make in how you work.

Alright, I’ll get off my documentation soapbox for now. Let’s write a few methods and see how this works in TouchDesigner.

We can start by creating a new text DAT called “text_simple_reutrn”, inside of this DAT we’ll write out our new functions:

def multi_by_two( value ):
    '''Multiplies input value by 2

    A simple example function to see how we can use modules on demand.
    This module takes a single argument which is multiplied by 2 and
    then returned from the function.

    Arguments
    --------------- 
    value( int / float ) - numeric value to be multiplied by 2

    Returns
    ---------------
    new_val( int / float ) - value * 2

    Notes
    ---------------
    These are doc strings - they're a feature of the Python language
    and make documenting your code all easier. This format is based largely
    on Google's Python documentation format - though not exactly. It's 
    generally good practice to document your work, leaving notes both for 
    your future self, as well as for other programmers who might be using
    your code in the future.
    '''
    new_val             = value * 2

    return new_val

def logic_test( even_or_odd ):
    '''Tests if input value is even or odd

    This is a simple little function to test if an integer is even or odd.

    Arguments
    --------------- 
    even_or_odd( int ) - an integer to be tested as even or odd

    Returns
    ---------------
    test( str ) - string result of the even / odd test

    Notes
    ---------------
    These are doc strings - they're a feature of the Python language
    and make documenting your code all easier. This format is based largely
    on Google's Python documentation format - though not exactly. It's 
    generally good practice to document your work, leaving notes both for 
    your future self, as well as for other programmers who might be using
    your code in the future.
    '''
    if even_or_odd % 2:
        test                = "this value is odd"

    else:
        test                = "this value is even"

    return test


def logic_test_two( value ):
    '''Silly logit test example

    Another simple function, this one to see another example of a 
    logic test working in a module on demand.

    Arguments
    --------------- 
    value( int / float / str / bool ) - a value to be tested

    Returns
    ---------------
    test( str ) - a string indicating the status of the test

    Notes
    ---------------
    These are doc strings - they're a feature of the Python language
    and make documenting your code all easier. This format is based largely
    on Google's Python documentation format - though not exactly. It's 
    generally good practice to document your work, leaving notes both for 
    your future self, as well as for other programmers who might be using
    your code in the future.
    '''
    if value == "TouchDesigner":
        test                = "Nice work"

    else:
        test                = "Try again"

    return test

Great! But what can we do with these? We can start by using some eval DATs or print statements to see what we’ve got. I’m going to use eval DATs. Let’s add several to our network and try out some calls to our new module on demand. First let’s look at the generic syntax:

mod( name_of_text_dat ).name_of_method

In practice that will look like:

mod( 'text_simple_reutrn' ).multi_by_two( 5 )
mod( 'text_simple_reutrn' ).multi_by_two( 2.5524 )
mod( 'text_simple_reutrn' ).logic_test( 5 )
mod( 'text_simple_reutrn' ).logic_test( 6 )
mod( 'text_simple_reutrn' ).logic_test_two( "TouchDesigner" )

Now we can see that we wrote several small functions that we can then call from anywhere, as long as we know the path to the text DAT we’re using as a module on demand! Here’s where we start to really unlock the potential of modules on demand. As we begin to get a better handle on the kind of function we might write / need for a project we can begin to better understand how to take full advantage of this feature in TouchDesiger.

Doc Strings

Since we took the time to write out all of those doc strings, let’s look at how we might be able to print them out! Part of what’s great about doc strings is that there’s a standard way to retrieve them, and therefore to print them. This means that you can quickly get a some information about your function printed right in the text port. Let’s take a closer look by printing out the doc stings for all of our functions:

# first let's clear the text port to make sure we're starting fresh
clear()

# Here we're printing out the doc strings for multi_by_two
print( "The Doc Strings for multi_by_two are:" )
print( '\n' ) 
print( mod( 'text_simple_reutrn' ).multi_by_two.__doc__ )

# Here we're printing out the doc strings for lotic_test
print( "The Doc Strings for logic_test:" )
print( '\n' ) 
print( mod( 'text_simple_reutrn' ).logic_test.__doc__ )

# Here we're printing out the doc strings for logic_test_two
print( "The Doc Strings for logic_test_two:" )
print( '\n' ) 
print( mod( 'text_simple_reutrn' ).logic_test_two.__doc__ )

That worked pretty well! But looking back at this it seems like we repeated a lot of work. We just learned about for loops, so let’s look at how we could do the same thing with a loop instead:

# first let's clear the text port to make sure we're starting fresh
clear()

# rather than wasting our time writing all the code in the other example, 
# instead let's write a for loop to automate that process.
# We'll start by first making a list of all of the methods we want to print 
# doc strings for
methods                         = [
                                "multi_by_two",
                                "logic_test",
                                "logic_test_two"
                                    ]

# next we'll make a smiple placeholder expression that we can 
# pass each method into so we can print it out easily
doc_string_temp                 = "mod( 'text_simple_reutrn' ).{target_function}.__doc__"

# finally we write a little for loop to go through all items in our list
# and pretty print their doc strings to the text port
for method in methods:
    print( "The Doc Strings for {} are:".format( method ) )
    temp_doc                    = doc_string_temp.format( target_function = method )
    print( eval( temp_doc ) )
    print( "= "  * 10 )

A Practical Example

This is all fun and games, but what can we do with this? There are any number of functions you might write for a project, but part of what’s exciting here is the ability to write something re-usable in Python. What might that look like? Well, let’s look at an example of a logger. There are a number of events we might want to log in TouchDesigner when we have a complex project.

In our case we’ll write out a method that allows a verbose or compact message, a way to print it to the text port or not, and a way to append a file or not. Alright, here goes:

import datetime

log_file            = op( 'text_log' )

full_text           = '''{now}

Current Year        | {year}
Current Month       | {month}
Current Day         | {day}
Current Hour        | {hour}
Current Minute      | {minute}
Current Second      | {second}
Current Microsecond | {microsecond}
'''

verbose_log_message = '''============================
VERBOSE MESSAGE

On {month}-{day}-{year} at {hour}:{minute}:{second}
----------------------------
operator            || {operator}
At Network Location || {path}

----------------------------
Logged
{message}
============================
'''

log_message         = '''----------------------------
{now}
----------------------------
{operator}
{path}
{message}
'''

def Full_date():
    '''Create a formatted time stamp

    A look at how we might create a formatted time stamp to use with
    various logging applications.

    Arguments
    --------------- 
    None

    Returns
    ---------------
    formatted_text( str ) - a string time stamp

    Notes
    ---------------
    '''

    now         = datetime.datetime.now()
    year        = datetime.datetime.now().year
    month       = datetime.datetime.now().month
    day         = datetime.datetime.now().day
    hour        = datetime.datetime.now().hour
    minute      = datetime.datetime.now().minute
    second      = datetime.datetime.now().second
    microsecond = datetime.datetime.now().microsecond

    formatted_text = full_text.format(
                                        now         = now,
                                        year        = year,
                                        month       = month,
                                        day         = day,
                                        hour        = hour,
                                        minute      = minute,
                                        second      = second,
                                        microsecond = microsecond
                                        )
    return formatted_text

def Log_message( operator, message, verbose=False, text_port_print=True, append_log=True ):
    '''Logging Method.

    A simple look at how you might start to think about building a logger for a TouchDesigner application. A logger is a great way to build out files with time stamped events. The more complex a project becomes, the more important it can become to have some means of logging the operations of your program. Here's a simple look at what that might look like.

    Arguments
    --------------- 
    operator( touch object ) - the touch object whose path you'd like included in the log message
    message( str ) - a message to include in the log
    verbose( bool ) - a toggle for verbose or compact messages
    text_port_print( bool ) - a toggle to print to the text port, or not
    append_log( bool ) - a toggle to append to the log file , or not

    Returns
    ---------------
    None

    Notes
    ---------------
    You'll notice that some arguments receive default values. This is so you don't have
    to include them in the call. This means that by default the message will be compact, 
    will print to the text port, and will append the log file.
    '''

    now         = datetime.datetime.now()
    year        = datetime.datetime.now().year
    month       = datetime.datetime.now().month
    day         = datetime.datetime.now().day
    hour        = datetime.datetime.now().hour
    minute      = datetime.datetime.now().minute
    second      = datetime.datetime.now().second
    microsecond = datetime.datetime.now().microsecond

    path        = op( operator ).path
    op_name     = op( operator ).name

    if verbose:
        message = verbose_log_message.format(
                                                month       = month,
                                                day         = day,
                                                year        = year,
                                                hour        = hour,
                                                minute      = minute,
                                                second      = second,
                                                operator    = op_name,
                                                path        = path,
                                                message     = message
                                            )
    else:
        message = log_message.format(
                                        now                 = now,
                                        operator            = op_name,
                                        path                = path,
                                        message             = message
                                    )

    if text_port_print:
        print( message )

    else:
        pass

    if append_log:
        log_file.write( '\n' + message )

    else:
        pass
    return

So now that we’ve written out the method, what would call for this look like?

operator = me

message ='''
Just a friendly message from your TouchDesigner Network.
Anything could go here, an error message an init message.

You dream it up, and it'll print
'''

# print and append log file with a verbose log message
mod( 'text_module1' ).Log_message( operator, message, verbose = True )

# print and append log file with a compact log message
# mod( 'text_module1' ).Log_message( operator, message )


# append log file with verbose log message
# mod( 'text_module1' ).Log_message( operator, message, verbose = True, text_port_print = False )


# print a compact log message
# mod( 'text_module1' ).Log_message( operator, message, append_log = False )

Take a moment to look at the example network and then un-comment a line at a time in the text DAT with the script above. Take note of how things are printed in the text port, or how they’re appended to a file. This is our first generalized function that has some far reaching implications for our work in touch. Here we’ve started with a simple way to log system events, both to a file and to the text port. This is also a very re-usable piece of code. There’s nothing here that’s highly specific to this project, and with a little more thought we could turn this into a module that could be dropped into any project.

Local Modules

We’ve learned a lot so far about modules on demand, but the one glaring shortcoming here is that we need the path to the text DAT in question. That might not be so bad in some cases, but in complex networks writing a relative path might be complicated, and using an absolute path might be limiting. What can we do to solve this problem. We’re in luck, as there’s one feature of modules we haven’t looked at just yet. We can simplify the calling / locating of modules with a little extra organization.

First we need to add a base and rename it to “local”, inside of this base add another base and rename it to “modules”. Perfect. I’m going to reuse one of our existing code examples so we can see a small change in syntax here. I’ve also changed the name of the text DAT inside of local>modules to “simple_return”.

mod.simple_return.multi_by_two( 5 )
mod.simple_return.multi_by_two( 2.5524 )
mod.simple_return.logic_test( 5 )
mod.simple_return.logic_test( 6 )
mod.simple_return.logic_test_two( "TouchDesigner" )
mod.simple_return.logic_test_two( 10 )

Looking at the above, we can see that we were able to remove the parentheses after “mod”. But what else changed? Why is this any better? The benefit to placing this set of functions in local>modules is that as long as you’re inside of this component, you no longer need to use a path to locate the module you’re looking for.

Alright, now it’s time for you to take these ideas out for a test drive and see what you can learn.

Python in TouchDesigner | Dictionary Loops | TouchDesigner

We’ve looked at dictionaries as data structures already, and have gotten a peak into how powerful then can be for storing and manipulating data. That’s all well and good, but wouldn’t it be lovely if we could loop through the contents of our dictionaries the same way we can loop through list items?

In fact, we can.

There are lots of ways to get there, so we’ll take a look at a variety of approaches to help us make this happen.

In this set of examples we’re going to use modules on demand as a fast way to get to our dictionaries that are stored in text DATs. We’ll take a closer look at how modules work later on, so for now let’s look at how we’re going to use them in this set of examples.

Here we can use the syntax:

mod( 'our_target_text_dat_here' ).our_target_variable

What does that mean exactly? This means we can put a python variable in a text DAT, and retrieve it with a mod call. For example, let’s say we have the following in a text DAT called “simple_mod_example”:

my_int = 5
my_float = 0.5
my_stirng = 'Hello there'

We can access these variables from any other operation with: mod( ‘simple_mod_example’ ).my_int mod( ‘simple_mod_example’ ).my_float mod( ‘simple_mod_example’ ).my_string

A little later one we’ll use some actual JSON that’s not in a module on demand as well – so with any luck by the end of this we’ll have a sense of how dictionaries work through and through. It’s also worth pointing out that python storage can also take advantage of dictionaries for fast variable access. So, once you have a handle on how this data structure works you’ll have a whole new world of options available to you.

Let’s first start with a a simple dictionary:

inventory = {
    "fruit" : {
        "apples" : 22 ,
        "kiwis" : 28 ,
        "grapes" : 11 ,
        "oranges" : 65 ,
        "grapefruit" : 75
    } ,
    "vegetables" : {
        "carrots" : 10 ,
        "potatoes" : 8 , 
        "onions" : 50 ,
        "kale" : 10
    } , 
    "beer" : {
        "ipa" : 32 ,
        "stout" : 46 ,
        "ale" : 56 ,
    }
}

Dictionaries are made of key and value pairs – the key being a string, and it’s pair being any data type of your choice. You start the construction of a dictionary with curly braces. Key value pairs are separated by a colon, and new entires are separated by commas. You’ll notice in the example above that we actually have a three dictionaries inside of a dictionary.

Let’s start by looking at just one:

fruit = {
    "apples" : 22 ,
    "kiwis" : 28 ,
    "grapes" : 11 ,
    "oranges" : 65 ,
    "grapefruit" : 75
}

This would be a fine way to create a single dictionary called fruit, with five entires, each with a quantity associated with them.

If we keep this in mind, looking back at our first example we can see that we have a single dictionary called “inventory” which contains a dictionary for fruit, vegetables, and beer. This nesting of key value pairs is a part of what makes these data structure so powerful. Where lists rely on an index based system to find an entry, dictionaries rely on keys. The major difference we can think about is that lists are ordered, and dictionaries are orderless. At first glance it might seem like an orderless data structure might not be useful, but the more we use them the more we’ll see how powerful they are.

Let’s look at our first code example, using modules means that we can use our dot notation to access an object inside of our text DAT.

inventory = mod( 'text_test_dictionary' ).inventory

# list of dictionary keys and their position
dictionary_keys = list( enumerate( inventory ) )

# let's get a feeling for how this works before we
# write a sentence
for item in dictionary_keys:
    print( item[ 0 ] , item[ 1 ] )

# loop through list and print the key and its
# list position
sentence = "The key {key} is in the {position} position of the list"

for item in dictionary_keys:
    print( sentence.format( key = item[ 1 ] , position = item[ 0 ] ) )

Okay, so what did we just do exactly? Well, we first made a list out of our dictionary keys, then we enumerated that list so we had an ordered construction with both the dictionary keys and a position, and then we printed all of that out. Why? We started by looking at how we might do this if we were working with a list. Isn’t that exactly what we’re trying to avoid? Yes, but since we already know how lists work this is a good place to get started so we can begin to better understand how these two things are different.

Okay, now that we’ve seen how this might work with an enumerated list, let’s look at key value loop. We’re going to use .items() to get the items out of our dictionary. We’ll first just look at how to get to the keys in our dictionary.

inventory = mod( 'text_test_dictionary' ).inventory

for dictionary_key , dictionary_value in inventory.items():
    print( "this dictionary key is: " , dictionary_key )

That’s a pretty good start, and makes it easy to see how keys work. But this is a different kind of loop here, so what else can we do? Let’s look at printing both our key and value this time around.

# define our variables
# using modules means that we can use our dot notation to
# access an object inside of our text DAT
inventory = mod( 'text_test_dictionary' ).inventory

for dictionary_key , dictionary_value in inventory.items():
    print( "this dictionary key is: " , dictionary_key )
    print( "the dictionary value for this key is: " , dictionary_value )

Alright, that’s pretty slick but our formatting still leaves something to be desired. Let’s add a few more lines and see if we can print out something a little nicer.

# define our variables
# using modules means that we can use our dot notation to
# access an object inside of our text DAT
inventory = mod( 'text_test_dictionary' ).inventory

# loop through the dictionary and print out contents
for dictionary_key , dictionary_value in inventory.items():
    # print out the first key depth in the dictionary   
    print( "- " * 10 )
    print( "this dictionary key is: " , dictionary_key )
    print( "in this dictionary we have:" )

    # loop through second dictionary layer
    for inventory_key , inventory_quantity in dictionary_value.items():
        # print out the key and value pairs from the second level
        print( '\t' , inventory_key , inventory_quantity )

Alright, not bad. Not bad at all. Here we can see that we ended up with a loop nested inside of our first loop. We started by printing out keys, then we run another loop to print out items and their quantities.

Recalling Presets

Okay, this is all well and good but what can we do with our new found tricks? Let’s start by doing something seemingly simple – filling out a table with the contents of our dictionary. Why put things into a table?! At some point you’ll certainly want to fill up a table with the contents of a dictionary… so we might as well look at how to do this early on.

# define our variables
# using modules means that we can use our dot notation to
# access an object inside of our text DAT
inventory = mod( 'text_test_dictionary' ).inventory
table = op( 'table_inventory' )
table_length = []
place_holder = 1

# first things first, let's clear out the contents of the table
table.clear()

# loop through the dictionary and add headers
for dictionary_key , dictionary_value in inventory.items():
    table.appendCol( [ dictionary_key ] )
    # add a blank column for next set of data points
    table.appendCol()

    # add to the list so we can determine the max length of the table
    table_length.append( len( dictionary_value ) )

# set the table length based on the maximum number of dictionary entries
table.setSize( max( table_length ) + 1 , table.numCols )

# loop through dictionary again to fill in table
for dictionary_key , dictionary_value in inventory.items():

# generate an enumerated list to loop through
    value_list = list( enumerate( dictionary_value ) )

# use enumerated list to fill in table 
    for item in value_list:
        table[ item[ 0 ] + 1 , dictionary_key  ] = item[ 1 ]
        table[ item[ 0 ] + 1 , place_holder ] = inventory[ dictionary_key ][ item[ 1 ] ]

# increment placeholder to ensure that our data goes in the right place
    place_holder += 2

Okay. Great. But how can we use this to actually do some interesting work?! Well, let’s begin by looking at how we might use these as presets. To get started let’s build out a simple little example. First we’ll add a movie file in TOP and call it ‘moviefilein_a’, connect this to a mono TOP called ‘mono_a’, and finally to a level TOP called ‘level_a’. We’re going to build out two sets of modules on demand for this example. First up is a set of default parameters for our TOPs. This is going to make sure that we can come back to a default position in our presents where nothing is on / nothing has changed in our pipeline. The idea here is that we can figure out the values to make sure that our chain of TOPs is not having any affect on our input texture.

level_defaults = {
    "invert" : 0,
    "blacklevel" : 0,
    "brightness1" : 1,
    "gamma1" : 1,
    "contrast" : 1,
    "gamma2" : 1,
    "opacity" : 1,
    "brightness2" : 1
}

mono_defaults = {
    "mono" : 0,
    "rgb" : 0,
    "alpha" : 4
}

Next let’s build out a set of cues that we can use to configure our little chain of TOPs.

cue_list = {
    "cue1" : {
        "file" : '/Map/Banana.tif',
        "mono" : 0 ,
        "invert" : 1,
        "brightness" : 0.71,
        "blacklevel" : 0.51,
        "contrast" : 1.93 
    } ,
    "cue2" : {
        "file" : '/Map/Smiley.tif',
        "mono" : 1 ,
        "invert" : 0,
        "brightness" : 1,
        "blacklevel" : 0.51,
        "contrast" : 1.5 
    } ,
    "cue3" : {
        "file" : '/Map/OilDrums.jpg',
        "mono" : 0,
        "invert" : 1,
        "brightness" : 0.71,
        "blacklevel" : 0.51,
        "contrast" : 1.93 
    }
}

You’ll notice that we’ve used two different approaches to name space here – that’s intentional. In our defaults we can see that we’ve made sure our key names match exactly to the parameter names on our TOPs. In our cues we can see that we’ve deviated from that a little. Why? Well, this will let us look at two different ways that we can target parameters when we’re using loops.

If we look back to our experiments with .pars() in looking at for loops, you might remember that we were able to us .pars() to quickly fill in a constant CHOP by ensuring that the contents of our table matched in terms of parameter names. This time around we can do the same thing by using our dictionary as our data structure instead of a table. We can then also quickly loop through the contents of our dictionary and assign values based on key names.

Let’s look briefly at how that might work.

First let’s look at how we might build a string to execute:

mono_defaults = mod( op( 'text_defaults' ) ).mono_defaults
level_defaults = mod( op( 'text_defaults' ) ).level_defaults

for keys, values in mono_defaults.items():

    #reset mono defaults for A 
    exec( "op( 'mono_a' ).par." + keys + " = " + str( values ) )

    # debut print so you can see what this is actually doing
    # print( "op( 'mono_a.par' )." + keys + " = " + str( values ) )

for keys, values in level_defaults.items():

    #reset mono defaults for A 
    exec( "op( 'level_a' ).par." + keys + " = " + str( values ) )

That works well, but as someone pointed out on the facebook help group, using exec() can cause a set of problems. It’s useful to know that we can build an expression as a string, and then execute that command – all dynamically, but we can also use pars():

mono_defaults       = mod( op( 'text_defaults' ) ).mono_defaults
level_defaults      = mod( op( 'text_defaults' ) ).level_defaults

for keys, values in mono_defaults.items():

    #reset mono defaults for A 
    op( 'mono_a' ).pars( keys )[ 0 ].val = values   

    # debut print so you can see what this is actually doing
    # print( "op( 'mono_a.par' )." + keys + " = " + str( values ) )

for keys, values in level_defaults.items():

#   #reset level defaults for A 
    op( 'level_a' ).pars( keys )[ 0 ].val = values

So far those seem like the same thing?! In this case, they’re pretty close. Because our key name matches our parameter name things are pretty straightforward here. Where it gets complicated is when our key names and parameter names don’t match.

Let’s use our cue_list dictionary to see how it might be different. Let’s imagine that we want to use cue2 as a preset. Now, because our key names and our parameter names don’t match, we have to write something like this:

movie_a                     = op( 'moviefilein_a' )
mono_a                      = op( 'mono_a' )
level_a                     = op( 'level_a' )
cue_list                    = mod( op( 'text_cue_list' ) ).cue_list

cue                         = 'cue2'

movie_a.par.file            = app.samplesFolder + cue_list[ cue ][ 'file' ]
mono_a.par.mono             = cue_list[ cue ][ 'mono' ]
level_a.par.invert          = cue_list[ cue ][ 'invert' ]
level_a.par.brightness1     = cue_list[ cue ][ 'brightness' ]
level_a.par.blacklevel      = cue_list[ cue ][ 'blacklevel' ]
level_a.par.contrast        = cue_list[ cue ][ 'contrast' ]

You’ll notice in the example above, we have to specify how each key and parameter relate to one another. In some cases this might be desirable. How?! You might ask. Well, lets’ imagine you have an AB system. In addition to our current set of operators, we’d also have movie_b, mono_b, and level_b. In this case it might be a little more tricky to know when to assign which parameter to which TOP. We might also end up in a situation where there are multiple level tops, so knowing when to target level1 vs. level2 might get a little more complicated. There are lots of ways you might solve these problems cleverly while still using pars(). It’s also worth pointing out that sometimes you want to be explicit in your code (probably more often than not), so you know exactly what’s happening. The important thing to remember here is that there’s a balance to find. You might not find it the first time you write your function or for loop, and it’s always okay to refactor your code to make it better.

Alright, let’s put some of these skills to use in an interesting way! We’ve used dictionaries as data structures, and we just learned how to loop through dictionaries. Let’s build out a set of buttons to recall our presets. I’m not going to go through how to make our buttons in detail – here are the cliff notes. Create a new container, add a button0 and give it the name “defaults”, next setup a replicator network to make three presets. Let’s make sure that our operator prefix is set to “preset” for our operator names when they get replicated. Finally, let’s make sure our buttons are set to radio. If all of this sounds like greek to you, make sure you go back and look at how replicators work first, and then look inside the example panel to get a better sense of how I set things up.

Okay! Now we’ll use a panel execute DAT (we just learned about panel executes) to recall our presets. We’ll use a logic test to run different loops depending on which button is pressed. In our case, Preset 1 – 3 will fill in our chain of operators with the presets from our cue_list dictionary. Clicking in the defaults button will return our chain of operators back to its default state. We can use the scripts we already wrote, and use the panel idea of the radio button selected to make this work. Let’s look at what that looks like:

# me - this DAT
# panelValue - the PanelValue object that changed
# 
# Make sure the corresponding toggle is enabled in the Panel Execute DAT.

mono_defaults       = mod( op( 'text_defaults' ) ).mono_defaults
level_defaults      = mod( op( 'text_defaults' ) ).level_defaults

movie_a             = op( 'moviefilein_a' )
mono_a              = op( 'mono_a' )
level_a             = op( 'level_a' )
cue_list            = mod( op( 'text_cue_list' ) ).cue_list

def offToOn(panelValue):
    return

def whileOn(panelValue):
    return

def onToOff(panelValue):
    return

def whileOff(panelValue):
    return

def valueChange(panelValue):
    # default radio button
    if panelValue == 0:
        for keys, values in mono_defaults.items():
            #reset mono defaults for A 
            op( 'mono_a' ).pars( keys )[ 0 ].val = values   

        for keys, values in level_defaults.items():                 
        #   #reset level defaults for A 
            op( 'level_a' ).pars( keys )[ 0 ].val = values  

    # for any of our preset buttons
    else:
        cue = 'cue' + str( panelValue )

        movie_a.par.file            = app.samplesFolder + cue_list[ cue ][ 'file' ]
        mono_a.par.mono             = cue_list[ cue ][ 'mono' ]
        level_a.par.invert          = cue_list[ cue ][ 'invert' ]
        level_a.par.brightness1     = cue_list[ cue ][ 'brightness' ]
        level_a.par.blacklevel      = cue_list[ cue ][ 'blacklevel' ]
        level_a.par.contrast        = cue_list[ cue ][ 'contrast' ]

    return

I know it’s seems like it’s been a long journey, but we’re now starting to see the real power of python in TouchDesigner.

Auto Configuration

Let’s quickly remember that with the Op Class we saw how we could create and destroy nodes. If we combine that with what we’ve just learned about using data structures and loops, we can begin to look at how we can automate the organization of a network.

Let’s imagine that we want to create a one toe file, and then depending on the IP address of a given machine we want TouchDesigner to set up different configurations. On one machine we might have two projectors, another might have three, another still might be our control machine. How can we make this work? Well, in this case we can store all of that information in a data structure like a python dictionary (you might also use JSON, or XML, or any data structure that you like). Next we’ll create a table based on our IP address, next we’ll loop through that table and then create our operators. Once you get a feeling for how this works with a table, you’ll quickly see how you could skip that step and just do this from the loop itself.

At the heart of this idea is the fact that you can store settings in your data structure, then build out your network based on that information. Alright, let’s dig in and get started.

A few things to keep in mind. Your IP address is going to matter. Remember that as you follow long here you’ll need to use your own IP address. You’ll notice that I’ve put a “default” address into a text DAT… this is so we can practice, but if you’re going to use this idea on a project you’ll need to familiarize yourself with the IP addresses, and have some control over how your machines are addressed.

Okay, to get started lets look at the configuration data we’ve set up:

uri = {
    "10.0.0.2" : {
        "name" : "mission_control" ,
        "role" : "controller" ,
        "displays" : [
            "control01" 
        ]
    },
    "10.0.0.3" : {
        "name" : "eyes" ,
        "role" : "node" ,
        "displays" : [
            "projector01" , 
            "projector02" 
        ]
    }
}

displays = {
    "control01" : {
        "width" : 1920 ,
        "height" : 1080 ,
        "orientation" : 0
    },
    "projector01" : {
        "width" : 1920 ,
        "height" : 1080 ,
        "orientation" : 0
    },
    "projector02" : {
        "width" : 1920 ,
        "height" : 1080 ,
        "orientation" : 1
    }
}

At a glance we can see that we’re working with two dictionaries – one called uri and the other called displays. These dictionaries have their own dictionaries nested inside, and have lists embedded inside as well. What’s going on here?! Our URI (uniform resource identifier) is a string key that corresponds to an IP address on a given machine. This key’s value pair is a dictionary instead of a single value. The contents of this dictionary are a name, a role, and a list of displays. For more complex configurations you might a much larger number of keys, but for now this should help us see what we’re up to. We also have a dictionary of displays. These contain keys related to resolution and orientation.

Alright, let’s put this information to good use. Before we get started we’re going to first add two more operators to our network. A table DAT called table_replicator_info and a text DAT called text_default_ip. We will fill in our table with the contents from our for loop process. For now we will use our default IP to see how this works.

Okay! Now let’s write a script to build out a network of ops:

# import any necessary modules

import socket

# define some variables

pos_x               = 3300
pos_y               = 0
replicator_table    = op( 'table_replicator_info' )
system_config       = mod( 'text_data' ).uri
displays            = mod( 'text_data' ).displays
local_ip            = socket.gethostbyname( socket.gethostname() )
default_ip          = op( 'text_default_ip' ).text
local_config        = {}
container_COMPs     = parent().findChildren( type = COMP , depth = 1 )

# clear contents of replicator_table
replicator_table.clear()

# delete existing container COMPs
for items in container_COMPs:
    if op( items ).name != 'container_presets':
        op( items ).destroy()
    else:
        pass

# Test for local_ip in system config
in_system_config = local_ip in system_config

# if our IP matches with a URI in the system config, then we
# fill our dictionary with keys based on the system config file
if in_system_config == True:
    local_config[ 'name' ]      = system_config[ local_ip ][ 'name' ]
    local_config[ 'role' ]      = system_config[ local_ip ][ 'role' ]
    local_config[ 'displays' ]  = system_config[ local_ip ][ 'displays' ]
    local_config[ 'uri' ]       = local_ip

# if our IP doesn't match our system config file, then we fill our
# dictionary with keys based on the default IP address, which is 
# entered in the text dat called 'text_default_ip'
else:
    # assign ip address
    local_ip = default_ip
    local_config[ 'name' ]      = system_config[ local_ip ][ 'name' ]
    local_config[ 'role' ]      = system_config[ local_ip ][ 'role' ]
    local_config[ 'displays' ]  = system_config[ local_ip ][ 'displays' ]
    local_config[ 'uri' ]       = local_ip

# fill table with display information
for items in local_config[ 'displays' ]:
    replicator_table.appendRow( [ 
        items , 
        displays[ items ][ 'width' ] , 
        displays[ items ][ 'height' ] , 
        displays[ items ][ 'orientation' ] 
        ] )

# check and correct height / width based on orientation flag
for row in range( replicator_table.numRows ):
    # create a temporary variable to store the width and height of the display
    width_height = [ replicator_table[ row , 1 ].val , replicator_table[ row , 2 ].val ]

    # test for orientation flag
    # if true, swap height and width
    if replicator_table[ row , 3 ] ==  1:
        replicator_table[ row , 1 ] = width_height[ 1 ]
        replicator_table[ row , 2 ] = width_height[ 0 ]

    # if false, we pass and do nothing
    else:
        pass

# create container COMPs based on our replicator table
for row in range( replicator_table.numRows ):
    # create our new container COMP, give it a name from our replciator table
    new_op = parent().create( containerCOMP , replicator_table[ row , 0 ] )

    # set the x position of the new container
    new_op.nodeX = pos_x

    # set the y position of the new container
    new_op.nodeY = pos_y

    # set the width of the new node based on the replicator table
    new_op.par.w = replicator_table[ row , 1 ]

    # set the height of the new node based on the replicator table
    new_op.par.h = replicator_table[ row , 2 ]

    # turn on the viewer flag
    new_op.viewer = True

    # increment the y position for the next loop
    pos_y -= 200

Why not just use a replicator?! That’s an excellent question. This might work just as well using a replicator – but, if we want to know exactly what order a set of operations happens in, and if we want to control when that operation fires then we need to write out a script to do just what we want. It might well seem like some additional work, but being able to work through the process line by line is worth at least learning even if you don’t end up using it much.

JSON

JSON, as a format is very similar to python dictionaries, so similar in fact there’s a very simple way to work with it.

If you’re already working with dictionaries this will feel like an easy transition. There are, however, a few things you need to keep in mind.

We need to make sure that our JSON is correctly formatted. If you compare the in our final example, with our first example you can see that we’ve enclosed our entire dictionary in curly brackets {}. You can also see that we’ve replaced our first variable with a key-value pair.

Let’s take a closer look:

# First we need to import the json module. 

import json

# next we're going to use just the text from our text dat
imported_dict       = op( 'text_simple_json' ).text

# next we'll use json.loads to import that as a python dictionary
dict_from_json      = json.loads( imported_dict )

# first let's just print the dictionary to make sure things worked out
print( "Here is our whole json object" )
print( imported_dict )

print( '_ ' * 10 )

# next we could print just the top tier keys
print( "Here are our top tier keys" )
for item in dict_from_json.keys():
    print( item )

print( '\n' )
print( '_ ' * 10 )
# next let's print the keys in our 'inventory' dictionary
print( "Here are the keys in inventory" )
for item in dict_from_json[ 'inventory' ].keys():
    print( item )

print( '\n' )
print( '_ ' * 10 )
# since we're on a roll, let's first print our keys, and
# then print their values
print( "Here are the keys and values in inventory" )
for key, value in dict_from_json[ 'inventory' ].items():
    print( key, 'contains', value )

print( '\n' )
print( '_ ' * 10 )
# That's still not very pretty, so let's see if we can make 
# something that's a little nicer
print( "Pretty printing our keys and values" )
for key, value in dict_from_json[ 'inventory' ].items():
    print( key, 'contains' )

    for item, quantity in value.items():
        print( quantity, item )

    print( '\n' )

With some easy ways to work with dictionaries under your belt challenge yourself to use these in an interesting way!

Python in TouchDesigner | For Loop | TouchDesigner

Loops are an indispensable concept when it comes to programming and there are a virtually endless number of uses for them that we might encounter. What exactly is a loop then? A loop is a way of thinking about any kind of process that’s repeated. Let’s first consider a simple example all of us might have encountered – washing dishes. This processes looks the same to all of us who may have found a sink full of dirty dishes waiting for attention. What do you do in this situation? Let’s assume that we don’t have dishwasher, and for the sake of simplicity let’s imagine that the act of washing the dishes is the same even if the dishes themselves are different. One by one, each dish is washed, rinsed, and dried. Let’s imagine that we already have functions for washing and drying dishes. If we were going to do this programatically for four dishes it might look like:

wash( dish1 )
dry( dish1 )

wash( dish2 )
dry( dish2 )

wash( dish3 )
dry( dish3 )

wash( dish4 )
dry( dish4 )

That’s not so bad, but what if we had 100 dishes, or 1000, or 10,000? That’s a lot of hard coding.

What if, instead, we could just make a list of our dishes:

[ dish1, dish2, dish3, dish4 ]

And then use a function to repeat the same procedure for every item in the list? Well, that’s what a loop is for. In this case, this is what a for loop is good for. A for loop will do the same thing, for every item in a list, or for every number in a range. In the case of our dishes let’s look at what that would mean for a list of dishes.

dishes = [ dish1, dish2, dish3, dish4 ]

for item in dishes:
    wash( item )
    dry( item )

And that’s it. If the list is 1 item long, or 10,000 items long, we don’t have to write any more code than that. Easy.

Okay, but what is a for loop?!

We might think of a for loop is a simple function that simply repeats the same series of actions in an iterative process – each successive item at a time. It’s easy for us to start by thinking of loops as being connected to lists, but they don’t have to be. They can be attached to a range – a range being a start and end point represented as integers. Instead of having a list of dishes, what if we only knew the number of dishes in the sink? How could we write this loop?

for item in range( 10 ):
    wash( item )
    dry( item )

Okay. That’s all well and good, but how can we start to think about this in a context that will help us?

Lets first make a simple list so we have something to work with

simple_list = [ 'apple', 'kiwi', 'orange', 'grape', 'pineapple' ]

The anatomy of our for loop can be a little confusing at first but once we get the hang of it, they’re very powerful.

Okay, so what does that syntax look like:

python for stand_in_variable in a_list_or_range: do each of these operations

let’s first imagine that we want to print out each of the items in our list

for item in simple_list:
    print( item )

Here we can see that ‘item’ is our stand in variable. For reach positional item in our simple_list, we print out that item.

What if we don’t have a list, and instead just want to run a loop a set number of times. Surely that’s possible, right?

In fact it is!

loop_count  = 10

print( "This is our first loop" )
for item in range( loop_count ):
    print( item )

That works great, but what’s a range?! Let’s print one out so we can better see what that’s about:

print( '\n' )
print( "This is the range of our loop_count" )
print( range( loop_count ) )

Printing this out we can see what we get a tuple with a starting and ending position in the list this might seem to imply that we can use a range, or list, but start at a position other than the first item.

print( '\n' )
print( "This is loop starting at position 5 going to the end" )
for item in range( loop_count )[ 5: ]:
    print( item )

print( '\n' )
print( "This is loop starting at the beginning and going to postiion 5" )
for item in range( loop_count )[ :5 ]:
    print( item )

Our simple list

simple_list = [ 'apple', 'kiwi', 'orange', 'grape', 'pineapple' ]

Sometimes we want to use the contents of a list but we also need to know the position of the item in the list. For that we can use the enumerate method that’s built into python. Let’s see it in action to better understand what we’re getting when we use enumerate.

print( "Here's our enumerated list" )
for item in enumerate( simple_list ):
    print( item )

Alright, that’s great, we can see here that we get a tuple of our list position, and our list item. That’s great, but how can we use that?

print( '\n' )
print( "Here's our enumerated list broken up a bit" )
for item in enumerate( simple_list ):
    print( item[ 0 ], item[ 1 ] )

Let’s imagine that we want to fill in something like a sentence with the information from our list.

message = "Item list position {}, actual item {}"

print( '\n' )
print( "Here's our enumerated list used with format" )
for item in enumerate( simple_list ):
    print( message.format( item[ 0 ], item[ 1 ] ) )

table = op( ‘table_simple_table’ )

Lists are all well and good, but how do we use them in TouchDesigner? First lets look at how we might use some tables.

One way to loop through our table’s rows would be to use the number of rows to define a range, then use our references to tables to move through its contents

print( "Here's one way to loop through our table" )
for item in range( table.numRows ):
    print( table[ item, 0 ] )

Another way to loop through our table would be to use the .col() method. This returns an object that’s a list of rows. To get the content of the cell, we need to use .val.

print( '\n' )
print( "Here's another way to loop through our table" )
for item in table.col( 0 ):
    print( item.val )

If we remember that channels are arrays of numbers, we can quickly see how we might move through one of this arrays with a loop.

First let’s remember that we can access the number of samples in a CHOP with .numSamples. We can use this value to determine our range.

We should take a quick moment to think about how we can access a single sample in a CHOP:

print( "let's look at the value at position 0" )
print( op( 'pattern1' )[ 'chan1' ][ 0 ] )

Let’s start with a simple task like printing out the value of each sample in a pattern CHOP.

First let’s simplify our code by using a variable to reference our pattern CHOP.

pattern = op( 'pattern1' )

Next we’ll write a simple for loop that runs based on the number of samples in a CHOP.

print( '\n' )
print( "let's print out every sample" )
for sample in range( pattern.numSamples ):
    print( pattern[ 'chan1' ][ sample ] )

Let’s go one step further and use .format() one more time to make for print statements that make more sense.

message = "{} is the value in the {} position"
noise   = op( 'noise1' )

print( '\n' )
print( "let's print out every sample in noise" )
for sample in range( noise.numSamples ):
    print( message.format( 
            round( noise[ 'chan1' ][ sample ], 3 ) , 
            sample
            )
        )

How else might we use for loops? We’ve seen how they might work with CHOPs and DATs, but what about TOPs?

We might imagine a circumstance where we wanted to fill a texture 3D – maybe for instancing, UI building or any number of things.

We can do this with a for loop easily, with just a little bit of thought.

# define some variables
text_DAT        = op( 'table_tex3d' )
text_TOP        = op( 'text1' )
tex3d_TOP       = op( 'tex3d1' )

for item in range( text_DAT.numRows ):
    # change the text 
    text_TOP.par.text               = text_DAT[ item, 0 ]

    # use a random number to set the background color
    text_TOP.par.bgcolorr           = tdu.rand( item )
    text_TOP.par.bgcolorg           = tdu.rand( item + 1 )
    text_TOP.par.bgcolorb           = tdu.rand( item + 2 )

    # set the repalce index to match the row
    tex3d_TOP.par.replaceindex      = item

    # pulse fill the texture 3D
    tex3d_TOP.par.resetsinglepulse.pulse()

Loops are useful for any number of processes. Let’s imagine that we want to fill up a table with the RGBA values from a TOP.

We could certainly use a TOP to CHOP, and then a CHOP to DAT, but we might imagine a circumstance where we don’t want this operation to happen all the time, only at times that we specify.

Or we might want to sample an image for colors to use for another process, and we don’t need a dedicated series of operators for this.

At any rate, lets look at how we might do this.

In this case I’ve set up a noise TOP to be 20 pixels tall, and 1 pixel wide. We can think of the number of pixels vertically as the range for our loop.

We’ll clear a table, and then append the contents for every pixel in our TOP. Easy.

First let’s use some variables to make writing our loop easeir:

nosie_TOP           = op( 'noise2' )
pixel_vals_DAT      = op( 'table1' )
header              = [ 'r', 'g', 'b', 'a' ]

In case there’s anything left in our table, let’s clear its contents first.

pixel_vals_DAT.clear()

Next let’s put some header information back into our dat, so we know what each colum is:

pixel_vals_DAT.appendRow( header )

Now we can loop through our pixels and append their values to our table.

for pixel in range( nosie_TOP.height ):
    pixel_vals_DAT.appendRow( nosie_TOP.sample( x = 0, y = pixel ) )

List comprehensions are a powerful means of constructing lists quickly. These might feel familiar from math courses you’ve taken or they may feel totally unfamiliar. In either case, they’re a wonderful tool to be able to use, and can make fast work for building or changing lists.

Let’s make a fast list so we can see the initial mechanics of list comprehensions

my_list = [ 5, 10 , 4 , 6 , 20, 13, 7, 31 ]

First let’s look at how we can print the contents of a list from inside the list.

print( "Let's start by just printing out the contents of the list" )
print( "- - - - - - - - - -" )
new_list1 = [ print( item ) for item in my_list ]

We can also print out the index of our item as we go

print( "\n" )
print( "Now lets look at how we can see the index of the items in our list" )
print( "- - - - - - - - - -" )
new_list2 = [ print( my_list.index( item ) ) for item in my_list ]

With a little bit of careful writing we can do both at the same time

print( "\n" )
print( "Now let's print both together" )
print( "- - - - - - - - - -" )
new_list3 = [ 
                print( 'the index of this item is: ' + str( my_list.index( item ) ) , 
                        'the acutal list item is: ' + str( item ) ) for item in my_list 
            ]

We can also construct a list from scratch, in this case we’ll make a list of each number in the list * 2 for the range of 10 If we write that out by hand we can see what we’ll expect from our comprehension: 0 * 2 = 0 1 * 2 = 2 2 * 2 = 4 3 * 2 = 6 4 * 2 = 8 5 * 2 = 10 6 * 2 = 12 7 * 2 = 14 8 * 2 = 16 9 * 2 = 18

Now that we know what we’re expecting to see, let’s see if it works the way we want

print( "\n" )
print( """Let's see how we can construct a list 
from scratch with a list comprehension""" )
print( "- - - - - - - - - -" )
list_from_scratch = [ item * 2 for item in range( 10 )  ]
print( list_from_scratch )

We can also construct a new list from a previous one. In this case let’s see if we can construct a list that’s only the even numbers from our first list.

print( "\n" )
print( "Let's see how we can construct a new list from an old one" )
print( "In this case, let's see if we can build one that's only even numbers" )
print( "- - - - - - - - - -" )
evens_only = [ item for item in my_list if item % 2 == 0 ]
print( evens_only )

We can also use loops to do all sorts of exciting things like create and place operators.

What we’ll quickly realize here is that for loops and the replicator COMP look very similar in the way they operate when it comes to creating ops!

You might feel like the replicator is good enough, so why learn how this work?! Sometimes knowing exactly how a process works can help us better understand another process.

In this case, setting up our own replicator script can teach us a lot about the replicator.

new_ops_list = [
    'text_newop1' ,
    'text_newop2' ,
    'text_newop3'
    ]

We’ll use this to determine the space between operators.

node_distance = 100

Loop through list

for item in enumerate( new_ops_list ):
    # create and name op 
    new_op = parent().create( textDAT , item[ 1 ] )

    # set location of nodes ( x or y )
    new_op.nodeX = me.nodeX
    new_op.nodeY = - ( item[ 0 ] * node_distance )

While we haven’t yet talked about the pars() we can take advantage of this member of the page class.

Any given operator with parameters has a page of parameters, with names and values. Knowing this we can find our way to the paramters of an op with some clever programming.

Let’s look at the anatomy of something like:

op( 'constant_pars_target' ).pars( 'name0' )[ 0 ]

pars() returns a list of parameters. For us, we can think of the above in plain english as the first list element in the parameter ‘name0’ from constant_pars_target.

Okay, so what can we do with that information?

Well, we might make a table of par names and values, and set them by looping through our table. Let’s take a look at how that might work.

First we’ll make a table called ‘table_pars_preset1’. Let’s fill that table with par names in one column, and par values in another.

Next we’ll create a stand in table where we can indicate which preset we want to use. Let’s call that ‘table_preset_selection’.

In the first cell let’s write ‘table_pars_preset1’. Alright, we’re almost ready to write our for loop.

Finally let’s add a constant CHOP called ‘constant_pars_target’

Now let’s write a loop!

First we’ll start with some variable names:

preset                              = op( 'table_preset_selection' )[ 0, 0 ]
target                              = op( 'constant_pars_target' )

Here in our loop is where things get intersting. For starters we’re going to use the op string name we’ve defined in our preset table.

for item in range( op( preset ).numRows )[ 1: ]:

    # As we go through the loop we'll use two variables one as our 
    # targed op_parameter, and another as a our parameter_value
    op_par                          = op( preset )[ item, 0 ]
    par_val                         = op( preset )[ item, 1 ]

    # Finally, we'll use those two varibles to change some varibles.
    target.pars( op_par )[ 0 ].val  =   par_val

    # We can use our preset statement to see how those two work together:
    print( 'target parameter: ', op_par )
    print( 'target value: ', par_val )

    # Let's also look at what that script would be if we were to write it
    # out by hand:
    script = 'op( "{op}" ).{par} = {par_val}'

    print( 'The script we run each loop:' )
    print( script.format( op = target.name, par = op_par, par_val = par_val ) )

    print( '\n' )

[Learn more about loops from Learn Python the Hard Way(http://learnpythonthehardway.org/book/ex32.html)]

Learn more about lists

Python in TouchDesigner | Op Class | TouchDesigner

The OP Class

Taking some time to really understand how you might take better advantage of classes when using Python in TouchDesigner is well worth the time and effort – even if frustrating and intimidating at first. Chances are, you’ve already used some class methods without even knowing it, and here we’re going to take a quick opportunity to better understand them, and how you might use them.

In the last example we looked at functions, and I mentioned that methods are also functions – in the same way that all squares are rectangles, but squares are a special kind of rectangle. Similarly, methods are a special kind of function. Special in that they belong to a class. In a highly simplified way, we might think of a class as a grouping a functions with a particular purpose. We can also use dot notation to access the members of a class. Let’s look at a simple example. For a hot second we’re going to depart from TouchDesigner and just talk about this problem as a programmer, then we’ll return to how this works and looks in Touch. Let’s imagine you want to put together a set of conversion tools. One approach would be to put all of your conversion functions together into one big class. If you’re only dealing with a few hundred lines of code that might be fine, but over time you’re likely going to need to keep updating this class, or you might find that it’s thousands of lines long suddenly and a bit unruly to wrangle. You might instead choose to separate functions into different classes that are thematically related. We might, in this case, choose to write a Temperature Conversion Class, and a Measurement Conversion Class as separate collections of code. That might look like this:

class TemperatureConversion():

    def F_to_C( self, temp_in_F ):

        temp_in_C = ( temp_in_F - 32 ) * ( 5/9 )

    return temp_in_C

    def C_to_F( self, temp_in_C ):

        temp_in_F = ( temp_in_C * ( 9/5 ) ) + 32

    return temp_in_F

class MeasurementConversion():

    def Inches_to_Centimeters( self, inches ):

        centimeters = inches * 2.54

    return centimeters

    def Centimeters_to_Inches( self centimeters ):

        inches = centimeters * 0.39

    return inches

So what’s the benefit here? Now when calling one of these functions we can use dot notation in order to get the results. For example:

print( TemperatureConversion.F_to_C( 50 ) )
print( TemperatureConversion.C_to_F( 100 ) )
print( MeasurementConversion.Inches_to_Centimeters( 12 ) )
print( MeasurementConversion.Centimeters_to_Inches( 1200 ) )

Organizationally, here we can easily see how our different classes give us a quick way to separate functions. Whew. Alright, that’s a lot of back-story in order to help us have a way to think about classes in TouchDesigner. We have to think / know about classes because that’s a part of organizational structure that we’re relying on when we use any dot notation for a method call. The Op Class applies to all operators in TouchDesigner, which means that the methods associated with it can be called in relation to any op – part of the reason we’re working through what that means.

Okay, let’s look at some examples. If you haven’t already looked at the wiki page about the Op Class, you should do that now.

In the next group of examples we’re going to use the Eval DAT in order to see how we can evaluate expressions quickly and easily. I frequently use the Eval DAT for just this reason, so I can see which parts of my expressions are working and which parts aren’t. Okay. Let’s first look at digits:

me.digits

Digits returns the integer number associated with an operator. In the example above we get the digits for the operator in question. In the example network it’s returning the digits for the operator table1.

Let’s look at another use of digits:

op( 'table2' ).digits

In this example we’re asking for the digits for table2. Now, it might seem a little useless to ask for the digits of an operator you already know the digits for, but it’s not hard to imagine a situation where this becomes very handy. This is especially useful when we use replicators.

parent().digits

The above, for example, is a great way to get get the digits of a parent. When using a replicator you might use this approach to increment through the rows of a source table.

Let’s look at some variations on the way you might retrieve the name of an operator:

parent().name
me.parent().name
op( '..' ).name
op( '/python_in_touchdesigner/example_op_class' ).name

All of the above return the same result. parent() is a method that accepts an argument for relational distance. Let’s say we wanted to get information from our grandparent component:

parent(2).name
me.parent(2).name
op( '../..' ).name
op( '/python_in_touchdesigner' ).name

Great, but what other kinds of methods can we use? Before the findOp DAT existed, you might use the findChildren() method to retrieve information about operators in a given component. In this case, I’m using a table generate rows for every operator in a component, and then using an Eval DAT to write one expression that’s uniquely evaluated for each row:

parent().children[ me.inputRow ]

Alright, one more time let’s go back to the wiki article on the Op Class. This time we’re going to take what we’ve learned about the Eval DAT, and what we’ve learned about classes to look at all of the methods we have access to for a text TOP. Let’s write out an expression for each of the methods:

'valid' op( 'text2' ).valid
'id'    op( 'text2' ).id
'name'  op( 'text2' ).name
'path'  op( 'text2' ).path
'digits'    op( 'text2' ).digits
'base'  op( 'text2' ).base
'passive'   op( 'text2' ).passive
'time'  op( 'text2' ).time
'activeViewer'  op( 'text2' ).activeViewer
'allowCooking'  op( 'text2' ).allowCooking
'bypass'    op( 'text2' ).bypass
'cloneImmune'   op( 'text2' ).cloneImmune
'current'   op( 'text2' ).current
'display'   op( 'text2' ).display
'expose'    op( 'text2' ).expose
'lock'  op( 'text2' ).lock
'selected'  op( 'text2' ).selected
'render'    op( 'text2' ).render
'viewer'    op( 'text2' ).viewer
'nodeHeight'    op( 'text2' ).nodeHeight
'nodeWidth' op( 'text2' ).nodeWidth
'nodeX' op( 'text2' ).nodeX
'nodeY' op( 'text2' ).nodeY
'nodeCenterX'   op( 'text2' ).nodeCenterX
'nodeCenterY'   op( 'text2' ).nodeCenterY
'inputs'    op( 'text2' ).inputs
'outputs'   op( 'text2' ).outputs
'type'  op( 'text2' ).type
'subType'   op( 'text2' ).subType
'label' op( 'text2' ).label
'family'    op( 'text2' ).family
'isFilter'  op( 'text2' ).isFilter
'minInputs' op( 'text2' ).minInputs
'maxInputs' op( 'text2' ).maxInputs
'isMultiInputs' op( 'text2' ).isMultiInputs
'visibleLevel'  op( 'text2' ).visibleLevel
'isBase'    op( 'text2' ).isBase
'isCHOP'    op( 'text2' ).isCHOP
'isCOMP'    op( 'text2' ).isCOMP
'isDAT' op( 'text2' ).isDAT
'isMAT' op( 'text2' ).isMAT
'isObject'  op( 'text2' ).isObject
'isPanel'   op( 'text2' ).isPanel
'isSOP' op( 'text2' ).isSOP
'isTOP' op( 'text2' ).isTOP

You’ll notice that I’ve separated the name of the method from the expression with a tab. This way when we feed our Eval DAT we get two columns – one with the name of the method, and another with the returned value.

You’ll notice that some methods are marked as ( Read Only ). This means that we can see information in calling these methods, but we can’t change anything about our Operator. Let’s look at an example of how we can make a change to an operator. Color is something we can change for any operator. I’m going to add three text DATs to my network. One operator to act on, and two text DATs where I’m going to write a simple script. First let’s change the color of our operator to red:

target_op = op( 'text1' )

target_op.color = ( 1 , 0  , 0 )

If we right click and run this script we’ll see that we’ve changed the color of our operator! Wait, let’s change it back:

target_op = op( 'text1' )

target_op.color = ( 0.5450000166893005 , 0.5450000166893005 , 0.5450000166893005 )

Perfect. This might seem like a silly example, but it brings to our attention how we might use various class methods to make changes to our networks. As a quick note, you might notice that I’ve written my scripts in two lines when I could have written them in one. Right? Why write:

target_op = op( 'text1' )

target_op.color = ( 1 , 0  , 0 )

When I could just write:

op( 'text1' ).color = ( 1 , 0  , 0 )

Part of the way that I work these days is to anticipate that I’m going to incorporate the pieces of a test script into a larger method or function. Separating the operator from the function call makes it much easier to begin thinking about how I might extend this simple script in the future. I could easily start to think of writing a function that looked like:

def Make_ops_red( op_path ):
    target_op = op( op_path )

    target_op.color = ( 1, 0, 0 )
    return

Okay, let’s look at one other interesting thing we might consider. What if we wanted to script the processing adding ops to a network? We can do just this with the copy method:

# create a new variable called new_op
# this is also a copy of the operator out 1
new_op = parent().copy( op( 'moviefilein1' ) )

# since we've defind our new op with the variable
# name new_op we can continue to use this name
# our next step will be to give it a name
new_op.name = 'moviefilein_new_op'

# finally we're going to change the location of
# our new operator. In this example we want it
# created at a location in relation to our original
# operator. We start by finding the original operator's
# y position, and then subtract 200
new_op.nodeY = op( 'moviefilein1' ).nodeY - 100

There are, of course, many more things you can do with the Op Class – my hope is that this helps you get a sense of where to start and pushes you to start experimenting a little more.

Python in TouchDesigner | Executes | TouchDesigner

Now that we know a little more about how functions work we’re ready to dig in a little deeper when it comes to working with execute DATs. There are a number of different executes, and they all work on a similar principle – a target operator is watched for changes, and when a change occurs a function is executed. Let’s first look at a very simple example with a CHOP execute.

The CHOP Execute DAT

Lets start by adding a Constant CHOP to our network, and then adding a CHOP Execute DAT. Next we drag the Constant onto the CHOP Execute DAT to specify that it’s our target CHOP. Okay… now what? Well right away we should see there are a number of functions in our CHOP Execute (some of this is redundant from the previous tutorial, so bear with me because it’s frightfully important).

As a quick note, you may see the functions in these DATs referred to as Methods. The semantics around function and method are nuanced in Python, and for the sake of clarity I’ll mostly refer to them as functions. The important thing to consider is that methods are functions – that happen to belong to a class. If this feels strange, we can remember that all squares are rectangles – but being square carries a particular classification. All of that to say that for right now, we can think of these as functions.

# me - this DAT
# 
# channel - the Channel object which has changed
# sampleIndex - the index of the changed sample
# val - the numeric value of the changed sample
# prev - the previous sample value
# 
# Make sure the corresponding toggle is enabled in the CHOP Execute DAT.

def offToOn(channel, sampleIndex, val, prev):
    return

def whileOn(channel, sampleIndex, val, prev):
    return

def onToOff(channel, sampleIndex, val, prev):
    return

def whileOff(channel, sampleIndex, val, prev):
    return

def valueChange(channel, sampleIndex, val, prev):
    return

Right away we should notice that we have a set of five functions in our DAT:

  • offToOn()
  • whileOn()
  • onToOff()
  • whileOff()
  • valueChange()

Okay. Let’s start by first unpacking what these functions do. As their names suggest, these are the conditions under which the function is called – that is to say when it will be executed. So what does that mean? Let’s take the example of offToOn(). In this case, the function offToOn is called when the CHOP’s value moves from the off position to the on position. In TouchDesigner, we can think of on as being any value that’s greater than 0. That means that anytime our value crosses the boundary of 0 to a positive number, this particular function will be called. To understand what that means we might make a simple change to the function like this:

def offToOn(channel, sampleIndex, val, prev):

    print( 'Hello there mischief maker' )

    return

Next we need to make sure our text port is open, and then we can move our Constant value from 0 to any positive number. We’ll see that every time we cross the 0 boundary, we get line printed to our text port. DAT Execute magic.

If offToOn() fires in the crossing from 0 into positive numbers, it’s not a stretch to realize that onToOff() fires when crossing into negative numbers. valueChange() will run whenever there’s a change in the value; whileOn() fires only when the value is above 0; whileOff() fires only when the value is below zero. If this still isn’t feeling useful yet, that’s okay – hang in there because it’s gonna get good.

That’s all well and good, but to better take advantage of our CHOP execute we should take a closer look at what’s happening. We’ll notice that each function has four arguments:

  • channel
  • sampleIndex
  • val
  • prev

So what are these things? Arguments are passed into the function and can be used in any way we like. In the header portion of our CHOP execute we have the following to give us a little guidance:

# me - this DAT
# 
# channel - the Channel object which has changed
# sampleIndex - the index of the changed sample
# val - the numeric value of the changed sample
# prev - the previous sample value
# 
# Make sure the corresponding toggle is enabled in the CHOP Execute DAT.

Here we can see that this arguments are objects, and values in their own right. Okay, so what can we do with these things? The answer is, of course, nuanced. Before we get too lost in the abstract possibilities of what we might do with these things, we should first return to our roots and do a little bit of printing to figure out what on earth we’re actually getting out of these arguments.

For starters we’re going to write out a little chunk of text with placeholders to values:

execute_text = '''
The channel is {}
The sampleIndex is {}
The val is {}
The prev is {}
'''

We’ll remember from earlier that we can use execute_text.format() to pass in some values to see printed out. If this is feeling only vaguely familiar you can revisit that tutorial here.

Okay. Next, let’s add a one-line print statement to our offToOn() function:

print( execute_text.format( channel, sampleIndex, val, prev ) )

That means we should have a DAT that looks something like this:

# Our first CHOP execute!

# Here we'll only look at the first function - offToOn.
# We already learned a little bit about functions, so we know
# that we have access to a set of arguments. Let's just print 
# those out so we can see what they are.

# me - this DAT
# 
# channel - the Channel object which has changed
# sampleIndex - the index of the changed sample
# val - the numeric value of the changed sample
# prev - the previous sample value
# 
# Make sure the corresponding toggle is enabled in the CHOP Execute DAT.

execute_text = '''\
The channel is {}
The sampleIndex is {}
The val is {}
The prev is {}
'''

def offToOn(channel, sampleIndex, val, prev):

    print( execute_text.format( channel , sampleIndex , val , prev ) )

    return

def whileOn(channel, sampleIndex, val, prev):
    return

def onToOff(channel, sampleIndex, val, prev):
    return

def whileOff(channel, sampleIndex, val, prev):
    return

def valueChange(channel, sampleIndex, val, prev):
    return

Now let’s change out constant CHOP a bit. Every time we cross from 0 to a positive number we should see a print statement that looks something like:

python >>> 
The channel is 0.05
The sampleIndex is 0
The val is 0.05000000074505806
The prev is 0.0

At first glance, channel and val are looking pretty similar. So what gives? Well, if we look back at our DAT we should see one particular line that tells us something very interesting:

# channel - the Channel object which has changed

Channel isn’t just a value, it’s an entire Python object. To better understand what that means we might want to look a little closer at the Channel Class page on the wiki. With a little closer inspection we can see that we can get all sorts of interesting information here: valid, index, name, owner, vals. These are all members of the Channel class, which means we can use dot notation to retrieve these. So, for example, we might change channel to channel.name in our simple example:

 print( execute_text.format( channel.name , sampleIndex , val , prev ) )

This means that now when we print we instead get this:

python >>> 
The channel is chan1
The sampleIndex is 0
The val is 0.25
The prev is 0.0

That might not yet seem important, but it means that we can use conditional statements in our functions to determine what happens. If we had a CHOP with 15 Channels, for example, but only wanted to run a particular portion of our function with ‘chan4’ changed, we now have a way to separate out this particular event.

Okay. That’s all well and fine, but what can we do with our new found python incantations? Let’s pause for a moment and think for about how we build networks in TouchDesigner, and how cooking works. Cooking, the nomenclature for calculation, is pull based instead of push based in Touch. This means that the last element in a chain pulls changes from upstream. This is often much more efficient, but it also means that we need to think carefully about how we program. Let’s consider a filter CHOP. The filter CHOP needs to always cook. Why?! Well, the filter CHOP’s job is to interpolate between two values, and in order to do that it needs to constantly be calculating changes. The result of this CHOP cooking constant means that any downstream node will also cook, always – even when it isn’t changing. In some circumstances this can cause challenges in facing efficiency in a network – especially if your filter CHOP is high up along the chain. Let’s look at a simple example.

Let’s add a few ingredients to our network, a buttom COMP, a filter CHOP, and a null CHOP. We can start by connecting these elements. Next let’s use the resulting single to drive an opacity change in a level TOP. If we used an expression or an export we can see out our export / expression line is constantly moving (meaning that it’s constantly cooking). We can also see that any operator downstream of our level CHOP (go crazy, and a few more TOPs to your network) is also cooking. That means by adding our single filter CHOP we’ve suddenly ended up with constantly cooking network. You may well throw your hands up in the air, and feel like them’s the breaks. But, that’s not the case. We can, instead, use a CHOP execute to drive a change in our Level TOP.

How?! If we remember that we can target parameters in operators, this suddenly becomes very easy. If you’re following along with our example network you’ll see that I want to target a TOP called ‘level2’. Let’s drop in a CHOP execute DAT, drag our null CHOP onto our DAT, and get to updating our function. I’m going to make the following change to our function:

# me - this DAT
# 
# channel - the Channel object which has changed
# sampleIndex - the index of the changed sample
# val - the numeric value of the changed sample
# prev - the previous sample value
# 
# Make sure the corresponding toggle is enabled in the CHOP Execute DAT.

def offToOn(channel, sampleIndex, val, prev):
    return

def whileOn(channel, sampleIndex, val, prev):
    return

def onToOff(channel, sampleIndex, val, prev):
    return

def whileOff(channel, sampleIndex, val, prev):
    return

def valueChange(channel, sampleIndex, val, prev):

    op( 'level2' ).par.opacity = val

    return

Did you catch it? It’s in the valueChange function, and it’s just a single line:

op( 'level2' ).par.opacity = val

Easy. This means that we’re only going to change that target parameter when the value changes from our target CHOP. Doing this we can see that we’ve now stopped forcing our TOP chain to cook constantly.

We might also think about using CHOP Executes to do things that simplify our networks. Let’s consider the following situation: we have a bank of movies that we want to switch between using a single button. Our button is connected to a count CHOP, and we’d like to cycle back through our four movies / images. One way to tackle this would be with a series of various CHOPs and DATs. In the example TOX I’ve used a count CHOP to cycle from 1 to 4. Next I converted this to a DAT, then inserted some text to match the format of our moviefile in TOPs, and finally converted to a text DAT before using this to drive a select TOP (that last step is a bit of a stretch, but I can see a situation where I might do something like that in a pinch). The resulting chain of operators all cook constantly. This happens, in part, because of our conversion from CHOP to DAT. Since our chopTo DAT is always cooking, this also means that everything down stream is cooking – always. How else might we solve this problem? Again, we might tackle this with a chop Execute DAT. In this case I’m going the use the integer value from the CHOP to help inform how I change the select TOP. Looking closer you can see the changes I’ve made to the value change function:

def valueChange(channel, sampleIndex, val, prev):

    target_path_v2 = 'moviefilein_picker_{}'
    op( 'select1' ).par.top = target_path_v2.format( int( val ) )

    return

Here I started by write a string with a placeholder for an inserted value:

target_path_v2 = 'moviefilein_picker_{}'

You’ll notice that all of the moviefilein TOPs have been renamed to “moviefilein_picker_1-4”.

The next sets the target select TOP with a path that’s formatted with an integer of the value from the chop – channels are floats by default, and we need to specifically convert this to an integer to make sure that our string formatting is correct.

op( 'select1' ).par.top = target_path_v2.format( int( val ) )

The Panel Execute DAT

The CHOP execute DAT is excellent, but it’s not the only execute option. The Panel Execute is one of my favorites, and an operator that I continually return to for all sorts of situations. Like the CHOP execute it fires based on a change in a target operator, in this case any COMP with a panel component – buttons, sliders, containers, tables, lists, etc. In the Panel Execute DAT we see the following when we plunk it down in a network:

# me - this DAT
# panelValue - the PanelValue object that changed
# 
# Make sure the corresponding toggle is enabled in the Panel Execute DAT.

def offToOn(panelValue):
    return

def whileOn(panelValue):
    return

def onToOff(panelValue):
    return

def whileOff(panelValue):
    return

def valueChange(panelValue):
    return

This should all look pretty familiar – after all it’s almost the same thing we saw in the CHOP execute. There is, of course, something we should pay close attention to right out the gate, our passed argument:

# panelValue - the PanelValue object that changed

Again, we can see that we’re actually passed an entire object, not just a single value. Looking at the wiki we can see that thepanelValue is a class in it’s own right. This means we have access to the name, owner, and val.

For starters let’s add a button COMP to our network, set it to momentary, and add a panel execute DAT. Next we can write a similar print statement to see what that means for us as we’re using the panel execute DAT:

# me - this DAT
# panelValue - the PanelValue object that changed
# 
# Make sure the corresponding toggle is enabled in the Panel Execute DAT.

def offToOn(panelValue):
    return

def whileOn(panelValue):
    return

def onToOff(panelValue):
    return

def whileOff(panelValue):
    return

def valueChange(panelValue):

    exec_text = '''\
-------------------------------
The PanelValue object contains:

panelValue.name = {}
panelValue.owner = {}
panelValue.val = {}
-------------------------------
'''

    print( exec_text.format( 
        panelValue.name,
        panelValue.owner,
        panelValue.val
         ) )

    return

Now when we click our button we should see something like the following printed:

The PanelValue object contains:

panelValue.name = v
panelValue.owner = /python_in_touchdesigner/example_executes/button3
panelValue.val = 0.3068181818181818
-------------------------------

Fancy! Again, this suddenly gives us access to a whole litany of possible directions to head when using this execute.

Last but by no means least, let’s look at how we might use several of these functions together. In this case I’m going to focus more on what’s happening in panel execute than the entire mechanics of what we’re up to. To understand what’s happening in terms of the idea, let’s consider that we want to do the following. I’d like to have a UI element where I can draw a shape that I then turn into a piece of geometry. There are any number of ways I might go about thinking through that problem. In this case I’m going to use a panel execute to set the position of a control element and extract the u and v values and append them to a table. I’m going to use those values to draw a box by converting CHOP data into a SOP. Finally, every time I start a new interaction on my UI, I’d like to clear the old data.

We start by first adding a container COMP, and then a smaller container COMP inside with a different color. If you’ve looked at how the slider COMP works, it’s the same idea at play here. We also need to add a table DAT where we can append some values. Next we’ll use the panel execute to make some magic happen. Let’s take a look at what we might write in order to get some interesting results.

# me - this DAT
# panelValue - the PanelValue object that changed
# 
# Make sure the corresponding toggle is enabled in the Panel Execute DAT.

# define some variables
ui = op( 'container1' )
box = op( 'container1/container_control' )
table = op( 'table4' )

def offToOn(panelValue):

    # clear our table when we start drawing a new shape
    table.clear()

    return

def whileOn(panelValue):

    # change the position of our contorl knob
    box.par.x = ui.panel.u  * ui.par.w - ( box.par.w * 0.5 )
    box.par.y = ui.panel.v  * ui.par.h - ( box.par.h * 0.5 )

    # add coordinates to a table to be turned into geometry
    table.appendRow( [ ui.panel.u , ui.panel.v ] )

    return

def onToOff(panelValue):
    return

def whileOff(panelValue):
    return

def valueChange(panelValue):
    return

The rest of this idea is then just about converting our table to a CHOP, and using a CHOP to SOP to further convert our data into geometry. At it’s heart, this is a simple idea, but without a panel execute DAT it might be a bit of headache to get functioning the way we want.

This, however, is just the tip of the iceberg – as it were. There are also DAT Executes (when tables changes), Parameter Executes (when parameters change), Executes (at start, and close of TouchDesigner), and OP executes (for changes in operator flags, names, etc.). Once you get started doing a little bit of Python scripting, the world is your oyster. Well, at least TouchDesigner is your Oyster.