1. NodeBox 1
    1. Homepage
    2. NodeBox 3Node-based app for generative design and data visualization
    3. NodeBox OpenGLHardware-accelerated cross-platform graphics library
    4. NodeBox 1Generate 2D visuals using Python code (Mac OS X only)
  2. Gallery
  3. Documentation
  4. Forum
  5. Blog

Stacks for context attributes

Posted by Josh Caswell on Dec 04, 2008

Hello,

I've got a couple of pieces of code here which I hope will be useful to someone. First, I wrote a class to make certain attributes of the Context class act like transforms already do. Each instance of this class has a stack (implemented as a list) and a user-chosen action, which points to a method of Context. The user can call pop() and push() on the instance, and values are shuffled around and set by calling action().
The three specific Context attributes I had in mind for this class were fill, stroke, and strokeWidth. I anticipate the stacks being useful for recursive drawing; with a global stack for them, you don't have to pass around the value of each from one level of recursion to the next.

class contextStack:
    """Handles a (user-chosen) attribute of the Context object 
    in the same way Context already handles transforms. 
	
    The user can push values (for, e.g., fillColor) onto a stack, and when 
    they are popped back off, the contextStack object 
    calls the appropriate method on Context to set the popped value.
    action can be any appropriate method of Context class from
    nodebox.graphics -- use yr judgement!"""
    
    def __init__(self, action):
        self.stack = []
        self.action = action
 
    def pop(self):
		try:
			self.action(self.stack.pop(0))
		except IndexError:
			print "Too many pops! No value change."
    
    def push(self):
		self.stack.insert(0, self.action())
 
The second piece of code is pretty trivial, and probably everyone already has already implemented their own version of it. I've defined an ellipse() function to mimic Processing's, taking the center of the shape as an argument, instead of the corner of the bounding box, which Cocoa wants. The bit that I really want to show off, which may be useful for more people, is adding the function to the _ctx object as a method, so you can call ellipse() at the top level of your end script, and this brings up a question that hopefully someone can answer for me.
def ellipse(x, y, width, height, draw=True):
    return _ctx.oval( x - width*0.5,
                      y - height*0.5,
                      width,
                      height,
                      draw )

I have these two pieces of code in a "conveniences" module in my Application Support folder. In my end script, I do an ximport() of the module, and then I want to do _ctx.ellipse = conveniences.ellipse, so I can use it at top level. conveniences doesn't have access to _ctx until after it's been imported, however, so I have this setup function also in the module:
# a stack for fill color
fillStack = None
# for stroke color
strokeStack = None
# and stroke width
strokeWidthStack = None
 
# note that fill(None) and stroke(None) are equivalent to nofill() and
# nostroke(), respectively, so there is no need for a separate on/off stack
 
# there is no _ctx object until after the module is imported,
# so we have to run this setup() function inside the end script to
# make instances and assign them to names
 
def setup():
 
    fillStack = contextStack(_ctx.fill)
    strokeStack = contextStack(_ctx.stroke)
    strokeWidthStack = contextStack(_ctx.strokewidth)
 
    # add these names to the context to make it a little
    # more pleasant to use them
    _ctx.ellipse = ellipse
    
    _ctx.fillStack = fillStack
    _ctx.strokeStack = strokeStack
    _ctx.strokeWidthStack = strokeWidthStack

And in the end script:
conveniences = ximport("conveniences")
conveniences.setup()

After the ximport(), conveniences.ellipse() will draw a shape correctly. The thing that I haven't been able to puzzle out is: the very first time I run a script with a top-level call to ellipse(), I get a NameError saying that ellipse is not defined. If I run the script again immediately, without changing a thing, it works perfectly.
If anyone's got an explanation of that, I'd be glad to know. Again, hope someone (maybe even Fred and Tom) finds this stuff useful.

--Josh Caswell
 
Posted by Stefan G on Dec 04, 2008

Suppose you have a module circletest you'd like to ximport:

def circle(x, y, w):
    return _ctx.oval(x, y, w, w)
 
def setup():
    _ctx.circle = _ctx._ns["circle"] = circle
you can then do:
ci = ximport("circletest")
ci.setup()
circle(10, 10, 140)
This will run the circle code from the first go.


Posted by Josh Caswell on Dec 06, 2008

Thanks, Stefan! Following the NB source around, I now think I see what is happening. Hadn't really grasped the significance of _ns in Context until you pointed it out. I appreciate the clue!