How to write a getTightBounds() for DirectGUI

I’ve written some code for laying out nodepaths, for example lining them up side-by-side, using getTightBounds() to get the boundaries of the nodepath objects. It works, but not with DirectGUI objects. I want to make DirectGUI compatible with my code.

If you call getTightBounds() on a DirectGui object you just get (0,0,0,0), which is why my code doesn’t work. Looking at DirectGuiBase.py, although DirectGuiWidget inherits NodePath, it actually keeps a list of nodepaths in self.stateNodePath. I’m guessing those nodepaths in the list are where the geometry for the widget in each of its states gets put, so the nodepath self does not hold any geometry so its bounds are 0.

DirectGuiWidget overrides getBounds() and returns the bounds of the nodepath for the current state:

    def getBounds(self, state = 0):
        self.stateNodePath[state].calcTightBounds(self.ll, self.ur)
        # Scale bounds to give a pad around graphics
        self.bounds = [self.ll[0] - self['pad'][0],
                       self.ur[0] + self['pad'][0],
                       self.ll[2] - self['pad'][1],
                       self.ur[2] + self['pad'][1]]
        return self.bounds

So getBounds returns some useful results for DirectGui objects. To make them compatible with my code a working getTightBounds is needed, so I tried to implement one based on getBounds in a class that wraps a DirectGui object:

class DirectGuiWrapper:

    def __init__(self, dgui):
        self._dgui = dgui

    def __getattr__(self, n):
        return getattr(self._dgui, n)

    def getTightBounds(self):
    
        l,r,b,t = self.getBounds()
        bottom_left = Point3(l,0,b)
        top_right = Point3(r,0,t)
        return (bottom_left,top_right)

As you can see it just takes the result of getBounds() and translates it to the (Point3,Point3) type that getTightBounds() normally returns for nodepaths. But this doesn’t seem to work, the DirectGui objects still don’t get lined up right by my layout code, which works fine for other nodepaths. I know I don’t fully understand DirectGuiBase.py. There’s something going on with self.guiItem, for one thing.

I don’t see anything obviously wrong. Are you sure your getTightBounds() method is actually being called, and that it’s returning something sensible?

David

A simple type check should do it. If it’s a Direct* or inherits from Direct*, use getBounds, else use getTightBounds.

def isDGUI(obj):
    DGWbase = obj.__class__.__bases__[0].__name__=='DirectGuiWidget'
    PGbase = obj.node().__class__.__bases__[0].__name__[:2]=='PG'
    for bc in obj.__class__.__bases__:
       if bc.__bases__:
          DGWbase |= bc.__bases__[0].__name__=='DirectGuiWidget'
    return DGWbase or PGbase

[EDIT] :
But once you use nested wrappers, the check process would be a bit complicated, since you have to check the base classes recursively.
A better way is relying on the C++ class instance finder, so you can go as crazy as you wish in Python.

def isDGUI2(obj):
    tmpNP = NodePath('temp')
    PGfound = not obj.instanceUnderNode(tmpNP,'').find('+PGItem').isEmpty()
    tmpNP.removeNode()
    return PGfound

Edit: Don’t waste your time reading this, I think I’ve fixed it. I changed the getRelativePoint calls. Here’s a question, if I call nodepath.getPos(), or nodepath.getTightBounds(), etc., in which coordinate system are the points returned? I had thought it was the coordinate system of that node’s parent node, but I think instead it might be in the coordinate system of the node itself. I think I need to read up a little on scene graphs.


Okay, still having trouble with this.

Class Box represents a box that can hold an item. Box wraps a nodepath, the nodepath that represents the box itself:

def __init__(self):
    ....
    self.np = aspect2d.attachNewNode('box')
    ....

For convenience various nodepath methods are forwarded directly to self.np, allowing a box object to be treated as if it were a nodepath object, for example getTightBounds():

    def getTightBounds(self,*args):
        return self.np.getTightBounds(*args)    

Box also defines these crucial methods bottom_left and top_right that are used by my layout classes to layout box objects:

    def bottom_left(self):
        # getTightBounds will return a bounding box that covers the nodepath and
        # all of its children.
        return self.getTightBounds()[0]        
    def top_right(self):
        return self.getTightBounds()[1]

As well as self.np, Box has another nodepath self.contents, this nodepath represents the contents of the box. Boxes can be layed out whether they are empty (self.contents = None) or full. When a nodepath is put into a box as well as setting box.contents = nodepath for later reference, the contents nodepath is reparented to the box nodepath box.np ( nodepath.reparentTo(self.np) ), therefore the bounds of the nodepath contribute to the bounds returned by box.bottom_left() and box.top_right() and all is fine. (The position of the contents nodepath is also set to (0,0,0) to line it up with the box nodepath self.np)

When you put a DirectGUI nodepath in a box there is a problem ofcourse, beause the bounds of a DirectGUI nodepath are 0, it fails to contribute to the bounds of the box, and the layout algorithms continue to work as if the box were empty.

So I tried to add a special case into the bottom_left() and top_right() methods using ynjh_jo’s type check:

    def isDGUI(self,obj):
        tmpNP = NodePath('temp')
        PGfound = not obj.instanceUnderNode(tmpNP,'').find('+PGItem').isEmpty()
        tmpNP.removeNode()
        return PGfound
                  
    def bottom_left(self):
        if self.isDGUI(self.contents):
            l,r,b,t = self.contents.getBounds()
            bottom_left = Point3(l,0,b)
            # The bounds should be in the coord space of the box node's parent node, not
            # that of the box node. (Remember that nodepath self.contents is parented to
            # nodepath self.np.)
            bottom_left = self.getParent().getRelativePoint(self.np,bottom_left)
        else: 
            bottom_left = self.getTightBounds()[0]
        return bottom_left        

    def top_right(self):
        if self.isDGUI(self.contents):
            l,r,b,t = self.contents.getBounds()
            top_right = Point3(r,0,t)
            top_right = self.getParent().getRelativePoint(self.np,top_right)
        else:
            top_right = self.getTightBounds()[1]
        return top_right

Still, though, it doesn’t work. These methods are certainly being called, execution is entering the isDGUI if statement and the special case result being returned, but the final result is just wrong.

For example, here are some nodepaths created by the CardMaker class, layed out in a grid by one of my layout classes:

This works great, I can change the size of the cards, change the size of the boxes, change the number, use various different layout algorithms, and it keeps working. Here is the exact same demo, but using DirectButtons instead of nodepaths from CardMaker:

The black squares behind the buttons are the nodepaths of the box objects that contain the buttons. The buttons should be side-by-side and top-to-bottom. I’m stumped.

The only and annoying difference of DGUI’s bounds is it’s relative to itself.

self.getParent().getRelativePoint(self.content,

And the worse thing is that bounds is the bounds without button’s relief, do you still remember ?
To include the relief, use :

self.content.node().getFrame()

It’s relative to itself too.

So dgui nodepaths return bounds relative to themselves, but nodepaths generally return bounds relative to their parent node as I had thought? I would write a little program to find out but I don’t have time, I must rush on. But does anyone have a link to a good primer on scene graphs in general? Although I think I mostly understand the concept in Panda it would probably be useful to read up on it.

Which returns the bounds in yet another different format. But it works, thanks ynjh_jo. The funny thing is that it looks like getBounds() actually gets the bounds with relief at first and then explicitly subtracts the relief before returning:

    def getBounds(self, state = 0):
        self.stateNodePath[state].calcTightBounds(self.ll, self.ur)
        # Scale bounds to give a pad around graphics
        self.bounds = [self.ll[0] - self['pad'][0],
                       self.ur[0] + self['pad'][0],
                       self.ll[2] - self['pad'][1],
                       self.ur[2] + self['pad'][1]]
        return self.bounds

I can’t possibly understand why one want to exclude the relief / pad. Just stick to getFrame, but I’ve never tried it with nested children.

It seems to be doing what I want now thanks once again to the help from you guys.