Creating New Nodes - The-Franklin-Institute/ARIEL_Builder GitHub Wiki
This document outlines how to create new nodes within the ARIEL Builder environment. To illustrate the process, we'll create a node that spits out a random number between a user defined range. Scroll to the bottom for the finished code.
Make the Scaffolding
Add to the Node Pallet
Create Inputs and Outputs
Player.py
Adding Parameters
setParameterDict and getParameterDict
We'll start by creating the scaffolding for our node in App/node.py
. The bare minimum we'll need to get going is a class that inherits Node
with a variable called self.label
. The label acts as the 'title' of the node in the environment and it is used as an identifier by the node pallet.
class RandomNumberGenerator(Node):
def __init__(self):
Node.__init__(self)
self.label = "random number"
Next we must tell the node pallet about our new class so that it appears in the menu. Head over to nodepallet.py
:
The dict labelsToNodes
contains key-value pairs with the pattern labelsToNodes["label name"] = node.ClassThatTheLabelRepresents
. Let's add our node to the bottom of the math + logic section:
labelsToNodes["random number"] = node.RandomNumberGenerator
Below this list you'll find a series of arrays:
sensorNodes = []
trackingNodes = []
displayNodes = []
mathLogicNodes = []
generalNodes = []
containerNodes = []
userNodes = []
arduinoNodes = []
Each item in the list will store the label name and the path to the label's icon image (as well as x
and y
variables which are deprecated). Let's add our node to the bottom of the mathLogicNodes
section, giving it the generic user icon icons/user.png
until we make a dedicated version:
mathLogicNodes.append(("random number", "icons/user.png", x, y))
Now the node pallet will recognize our node when the software starts up. Open ARIEL Builder and you'll be able to create your node from the node pallet!
Before we write the actual function for our random number generator, let's give it some inputs and outputs so we can control what it does.
We'll make two inputs: a minimum and a maximum number to give the user control over the randomness. Then we'll make an output: the random number itself.
Each input must have a type associated with it. Since we're dealing with numbers we'll be using the NumberInput
class to make our inputs. The full list of input and output types is available within node.py
. In our RandomNumberGenerator
class:
class RandomNumberGenerator(Node):
def __init__(self):
Node.__init__(self)
self.label = "random number"
self.addInput(NumberInput(self, "minimum"))
self.addInput(NumberInput(self, "maximum"))
self.addOutput(NumberOutput(self, "result"))
We add inputs using self.addInput(NodeInput(self_reference, "label of input"))
. Outputs follow the same pattern with the NodeOutput
subclasses.
Finally we'll call self.doSimplePropertyWindow("")
, which creates a default "This node has no properties" message in the properties window.
class RandomNumberGenerator(Node):
def __init__(self):
Node.__init__(self)
self.label = "random number"
self.addInput(NumberInput(self, "minimum"))
self.addInput(NumberInput(self, "maximum"))
self.addOutput(NumberOutput(self, "result"))
self.doSimplePropertyWindow("")
If you try to run a sketch with this node, you'll notice that it crashes. That's because we've only created the GUI portion of the node thus far. Let's go over to player.py
to write the operation that our node will perform.
Our class will have the same name– this is how the compiler turns GUI nodes into player.py
nodes.
Within __init__()
we'll create our inputs and outputs and add them to two lists: self.inputs
and self.outputs
:
class RandomNumberGenerator(Node):
def __init__(self):
Node.__init__(self)
self.minimumInput = NodeInput("minimum")
self.maximumInput = NodeInput("maximum")
self.inputs = [self.minimumInput, self.maximumInput]
self.resultOutput = NodeOutput("result")
self.outputs = [self.resultOutput]
Notice that there are no input and output types on the player.py
end; all inputs and outputs are simple NodeInput
or NodeOutput
.
Next we'll need to define the compute()
function that gets the work done. We'll use openFrameworks' ofRandom(float min, float max)
function to generate our number.
If an input has nothing connected to it, its value will be None
. Since ofRandom()
won't accept None
as a parameter, we'll need to set a default value in the case that nothing is coming into our node.
def compute(self):
if self.minimumInput.value == None:
self.minimumInput.value = 0
if self.maximumInput.value == None:
self.maximumInput.value = ofGetWidth()
self.resultOutput.value = ofRandom(self.minimumInput.value, self.maximumInput.value)
That's it! We've just written a fully-functioning node.
Let's extend our node in a new way by adding a parameter to the Property Window. This will pop up when we double-click the node, and it can contain GUI elements that allow us to extend the functionality and customization of the node without adding an unreasonable amount of inputs to it.
Let's add a number input that lets us control how often, in seconds, a new number is generated.
class RandomNumberGenerator(Node):
def __init__(self):
Node.__init__(self)
self.label = "random number"
self.addInput(NumberInput(self, "minimum"))
self.addInput(NumberInput(self, "maximum"))
self.addOutput(NumberOutput(self, "result"))
titleLabel = gui.Label(self.propertyWindow)
titleLabel.font = fontmanager.manager.getFont("Frutiger-Bold.ttf",14)
titleLabel.setMessage("random number")
titleLabel.setPosition(10, 0)
titleLabel._setW(300)
numberLabel = gui.Label(self.propertyWindow)
numberLabel.font = fontmanager.manager.getFont("Frutiger-Roman.ttf",10)
numberLabel.setMessage("frequency of new result in seconds")
numberLabel.setPosition(10, 30)
numberLabel._setW(300)
self.secondsInput = gui.TextEntry(self.propertyWindow)
self.secondsInput.setPosition(10, 60)
self.secondsInput.w = 60
self.secondsInput.currentText = "0.1"
self.secondsInput.validateAsNumber()
We create each element– adding it to the node's propertyWindow
in the process– and set its various parameters. Labels are static and we won't be interacting with them, so we don't need to store a reference to them. Interactive elements such as gui.TextEntry
, however, must be stored in a variable so that we can access the data inside of them later.
Most of the GUI element parameters deal with their layout in the Property Window. gui.TextEntry
has an option to require that the input be a valid number, which we use by calling gui.TextEntry.validateAsNumber()
.
At this point we have a working Property Window. Before we can access the input from player.py
, however, we'll have to use setParameterDict
and getParameterDict
to pass the input value along through the compiler.
These two functions allow us to pass values into our player.py
node, where they will be come class variables.
setParameterDict
sets the value of interactive inputs when a file is loaded, while getParameterDict
exports the input values during the process of saving and/or running a sketch.
def getParameterDict(self):
d = Node.getParameterDict(self)
d["secondsInput"] = float(self.secondsInput.currentText)
return d
When this function is called, the return type must by a Python Dictionary that stores any values you want to be able to access from player.py
(or from a saved file). We store the secondsInput
file at the key d["secondsInput"]
before returning the dictionary.
def setParameterDict(self, d):
try:
Node.setParameterDict(self, d)
self.secondsInput.currentText = `d["secondsInput"]`
except KeyError:
print self.label,"had a backwards compatibility issue. Check to make sure all the parameters are correct."
When this function is called a Python Dictionary object is passed in. It should contain the values that we stored within getParameterDict
; in this case, d["secondsInput"]
should contain a number. We encapsulate it within a try/except statement just in case it doesn't exist. This is most often the case when a file is opened by a newer version of the software that contains a version of the node expecting more parameters than it finds.
Now that we've stored some input, head over to player.py
where we will create a new variable called secondsInput
:
class RandomNumberGenerator(Node):
def __init__(self):
Node.__init__(self)
self.minimumInput = NodeInput("minimum")
self.maximumInput = NodeInput("maximum")
self.inputs = [self.minimumInput, self.maximumInput]
self.resultOutput = NodeOutput("result")
self.outputs = [self.resultOutput]
self.secondsInput = 0
def compute(self):
if self.minimumInput.value == None:
self.minimumInput.value = 0
if self.maximumInput.value == None:
self.maximumInput.value = ofGetWidth()
self.resultOutput.value = ofRandom(self.minimumInput.value, self.maximumInput.value)
During the compiling process, the secondsInput
variable will be given the value stored within setParameterDict
. It is crucial that the name of the variable in player.py
matches the key value of the dictionary.
Adding a little more logic to deal with timing, we end up with this:
# player.py
class RandomNumberGenerator(Node):
def __init__(self):
Node.__init__(self)
self.minimumInput = NodeInput("minimum")
self.maximumInput = NodeInput("maximum")
self.inputs = [self.minimumInput, self.maximumInput]
self.resultOutput = NodeOutput("result")
self.outputs = [self.resultOutput]
self.secondsInput = 0
self.lastTimeStamp = 0
def compute(self):
if self.minimumInput.value == None:
self.minimumInput.value = 0
if self.maximumInput.value == None:
self.maximumInput.value = ofGetWidth()
t = time.time()
if t - self.lastTimeStamp > self.secondsInput:
self.lastTimeStamp = t
self.resultOutput.value = ofRandom(self.minimumInput.value, self.maximumInput.value)
# node.py
class RandomNumberGenerator(Node):
def __init__(self):
Node.__init__(self)
self.label = "random number"
self.addInput(NumberInput(self, "minimum"))
self.addInput(NumberInput(self, "maximum"))
self.addOutput(NumberOutput(self, "result"))
titleLabel = gui.Label(self.propertyWindow)
titleLabel.font = fontmanager.manager.getFont("Frutiger-Bold.ttf",14)
titleLabel.setMessage("random number")
titleLabel.setPosition(10, 0)
titleLabel._setW(300)
numberLabel = gui.Label(self.propertyWindow)
numberLabel.font = fontmanager.manager.getFont("Frutiger-Roman.ttf",10)
numberLabel.setMessage("frequency of new result in seconds")
numberLabel.setPosition(10, 30)
numberLabel._setW(300)
self.secondsInput = gui.TextEntry(self.propertyWindow)
self.secondsInput.setPosition(10, 60)
self.secondsInput.w = 60
self.secondsInput.currentText = "0.1"
self.secondsInput.validateAsNumber()
def setParameterDict(self, d):
try:
Node.setParameterDict(self, d)
self.secondsInput.currentText = `d["secondsInput"]`
except KeyError:
print self.label,"had a backwards compatibility issue. Check to make sure all the parameters are correct."
def getParameterDict(self):
d = Node.getParameterDict(self)
d["secondsInput"] = float(self.secondsInput.currentText)
return d