Subclassing Fu - Yet Another Thread :)

I am in the whole “do I wrap or subclass” stage.
I have been all over the forums; I guess I have a stupid-streak, I just want this to be pythonic. So, given that I may have to dump it all and play with wrapping NodePath instead, let’s see what we can make of the code below.

from pandac.PandaModules import *
import direct.directbase.DirectStart


class DNodePath(NodePath):
	def __init__(self, *args, **kwargs):
		## If no *args, then this class will NOT work! 
		NodePath.__init__(self, *args)
		self.instance = "instance" # calls setter
	## Gratuitous decorator frenzy, move along...
	@property
	def instance(self):
		return NodePath.getPythonTag(self._tag)
	@instance.setter
	def instance( self, tag ):
		self._tag = tag
		NodePath.setPythonTag(self, tag, self)
	@instance.deleter
	def instance(self):
		NodePath.clearPythonTag( self, self._tag )
		self._tag = None
		print "instance deleted"

class ModelThing( DNodePath):
	def __init__(self, parent, file):
		_nphandle = loader.loadModel("/usr/share/panda3d/models/%s" % file)
		## Make sure we pass a PandaNode in: this one's node() is a <type 'libpanda.ModelRoot'>
		## This causes ModelThing to BE A ModelRoot type.
		DNodePath.__init__(self, _nphandle)
		self.reparentTo( parent )
		self.blah = "blah"

def cast( nodepath ):
	if nodepath.hasPythonTag("instance"):
		return nodepath.getPythonTag("instance") # return my actual subclass
	return nodepath # It's not one of my subclasses.

def walkGraph(node):
	node = cast(node)
	print node, type(node), type(node.node())
	for child in node.getChildren():
		walkGraph(child) 

base.disableMouse()
base.camera.setPos(0,-100,0)
base.camera.setHpr(0, 0, 0)
base.camLens.setFar(9000)
base.camLens.setFov(55)

node1 = render.attachNewNode("node1")

p = ModelThing( node1, "panda")
print "###",repr(p),type(p), type(p.node())

## Find a pure NodePath object from the SG, so
## I can experiment with the poor bugger:
t = cast( render.find("node1/panda.egg") )
print "t:", t, type(t), type(t.node()), t.blah

walkGraph(render)

node1.removeNode() ## <-- WTF?!

run()

If I make a ModelThing class and add it to render, it does ye olde fake C++ object wrapped in a distant Python cousin that is not welcome in the valley trick. So I have PythonTag (inherited through DNodePath) pointing back at my class-instance and I use the cast() function to fetch it when I have only a pure NodePath to work with.

All fine so far. I think.

But, how do I ensure that my ModelThing (and all children) are removed when I use something like somenode.removeNode()?
It wipes-out the nodes on the scene-graph, great, but I want it to also bump-off those awkward cousins, the ModelThings…

I suppose I could write my own removeNode and perform some recursive-fu but I am not sure where to begin really.

Is there some way to get a NodePath to call-back to the object pointed to in PythonTag? That would rule.
Failing that, is there any event or signal that a NodePath emits that can be detected anywhere in Python? I could use that to target a ModelThing and pull the trigger myself.

\d

One answer is to rely on Python’s reference counting: when the last reference to your ModelThing goes away, then the ModelThing itself will go away, automatically.

The problem with this is that it won’t work in this case, because you have established a circular reference count: ModelThing has a reference to the NodePath instance, which holds a reference to its underlying PandaNode instance, which holds a reference back to your ModelThing (due to the setPythonTag). So there’s no way for Python to break this chain. (The Python garbage collector can’t do it either, because of inherent limitations in Python’s design. But that’s another topic.)

We don’t have an automatic callback system to call into Python when the node is destructed. We could add one, but it wouldn’t work either, because it would never be called: Panda uses a reference-counting system similar to Python’s and that same reference-count cycle would prevent Panda from ever destructing the node and making that callback.

Note that NodePath.removeNode() doesn’t actually delete a node. All it does is break the connection with the node’s parent, and then empty the NodePath itself. If that was the only reference to the node (and it often is), the node will be deleted; but if the node has other parents, or other NodePaths referencing it, it will persist. This is by design; it is functionally similar to Python’s “del” operator. Not to mention that removeNode() isn’t the only way to remove a node’s parent in the first place. So, hooking into removeNode() wouldn’t be reliable in all cases.

So, at the end of the day, you will need to have something to explicitly break that cycle in order to delete the node. One answer is to overload removeNode() in your ModelThing class to do the right thing. Then, as long as you are responsible to always call cast() on any NodePath you get back from the scene graph, and you never use any mechanism other than removeNode() to clean up these objects; and you never rely on the NodePath automatically getting cleaned up because its parent was cleaned up, well, then you’re golden. :wink:

Obviously it is not an answer in the general case.

There might be another answer, by relying on so-called weak reference counting. As long as one of the reference counts in the chain is a weak reference count, the chain can be broken automatically when the last external link goes away. Both Python and Panda offer a weak reference count object, though Panda’s isn’t exposed to Python. You might be able to wrap the “self” that you pass to NodePath.setPythonTag() in a Python weakref object, though.

David

Or you could just include your nodepath as an attribute in the class, and not have to think about this any more.

My thought is to use the SG and not have to make my own ghost-tree/SG off to the side with my custom objects. It seems to me that the SG already does traversal and pruning and parenting for me – an awesome amount of work – so why do it all again.

I want to hang my own nodes on the SG. This is not possible for all the stated reasons. So I must have a pythonTag in each SG node that points to my actual class.

My options:
Wrap nodepath as attribute. Do not use Pythontag
I can’t use my class as a nodepath. I can’t step up and down or removeNode etc.; not without writing all that code myself.
Wrap or subclass and employ pythonTag
Circular ref problem and other subtle things.

What to do?

I have a feeling that I have missed something about nodePaths. What happens to the children of a node when removeNode is called? Do they all get visited? I have the feeling they do not.

Nevertheless, would this not be a good place to make a callback? The node is being emptied, it can look for a pythonTag and call etTuBrute() on that Python object. Or something.

I guess what I am angling at is a way to make Python objects feel more like SG objects and get signals from the SG when important stuff is happening.

I actually tried that but it did not work. I think I know why, so I will try again.

/me panic sets in as the boundaries of what I know get stretched :slight_smile: I will hit the Python docs when I get a chance.

Thanks for the gen.
\d

I say it again: nodePath.removeNode() does not delete the node. It only dereferences it. If it removes the last reference to the node, though, then the node will be deleted, which will also dereference all of the node’s children. If that was the last reference to any of the node’s children, then they will be removed as well; and the whole thing cascades down. But there was only one call to removeNode().

I think you might have a bit of the classic NodePath vs. node confusion that every newcomer to Panda experiences. Although we normally interact with NodePaths, they are really only handles to nodes, they are not nodes themselves. NodePath does not inherit the properties of its underlying node; a NodePath that references a GeomNode does not have additional methods that allow you to control the Geom’s vertices. And the node doesn’t have a back pointer to its NodePath, and can’t make callbacks into it. (This is actually only about 75% true, but that’s an implementation detail.)

The most important takeaway from all this is that there is no tight correlation between NodePath.removeNode() and the actual deletion of nodes. Calling removeNode() doesn’t necessarily remove the node; and the node can be deleted without removeNode() ever having been called. So although we could have a callback on NodePath.removeNode(), it wouldn’t really do what you want. We could put the callback on the actual deletion code, but that wouldn’t do what you want either.

If your purpose is to hang additional data on the scene graph, maybe you should consider doing this without a subclassing model, and without the circular referencing that that requires. Instead, just use setPythonTag() to store your data on each node, but don’t also try to store a back pointer to the same node. Then reference counting will work exactly as intended.

In other words, stop thinking about an “is-a” relationship, and think in terms of a “has-a” relationship instead. The “is-a” relationship is nothing but trouble here, but “has-a” works perfectly.

And the “has-a” relationship can go in either direction. If you want to take advantage of Panda’s existing scene graph to store your data, then store your data as attributes of nodes. Just don’t try to make your data into nodes.

David

Thanks David, this will bear some careful reading and experiment. I need to get dirty!

\d

hmm, you make me wonder if i understand the concept correctly:

n0 = loader.loadModel('box')
n0.reparentTo(render)
n0.removeNode()  # this node gets destroyed

n1 = loader.loadModel('box')
n1.reparentTo(render)
n2 = loader.loadModel('box')
n2.reparentTo(n1)
n1.removeNode()   # it's not getting destroyed yet
n2.removeNode()   # now n1 and n2 get destroyed

is this correct?

for the setPythonTag problem i recommend using a setTag with a identifier (for example “hash(self)”, if you dont need to save/load, othervise a counter) for the nodepath, and having a objectIdManager that you can get the id or class from. using this you never have circular references.

Almost.

n0 = loader.loadModel('box')
n0.reparentTo(render)
n0.removeNode()  # this node gets destroyed

Correct.

n1 = loader.loadModel('box')
n1.reparentTo(render)
n2 = loader.loadModel('box')
n2.reparentTo(n1)
n1.removeNode()   # it's not getting destroyed yet

Incorrect: n1 is immediately destroyed at this point. Parents hold references to their child nodes, but not the other way around–so there are no outstanding references to n1, and it is free to be destroyed. At this point, n2 remains as an orphan node (it no longer has a parent).

n2.removeNode()   # now n1 and n2 get destroyed

n2 is destroyed at this point.

David