Drag-dropping NodePaths

I had a quick search of the forums, but didn’t find anything. I want to imlement drag-drop with nodepaths that contain some visible and collidable geometry (geometry produced by the CardMaker class). They are on Panda3D’s 2D scene graph rather than in 3D. Thinking about it, it seems entirely possible to implement a drag-drop system, but not an insignificant amount of work. I just thought I’d see if anyone had done it before or had any pointers, before I dive into it.

Here’s a more precise description of what I want to happen. The ‘cards’ in this description are nodepaths with some visible and collidable geometry from CardMaker attached to them.

  • Have cards highlight themselves when the mouse rolls over them, to show that they are draggable. (I’ve already done this bit.)

  • When the user presses a mouse button down detect which card they are clicking on (I’ve done this also) and begin dragging it

  • While the mouse button is down have this card be dragged around by following the mouse cursor around the screen. (Should be easy.)

  • If the card is dragged over any other cards, have them highlight themselves to show that they can be dropped onto. (Should be just the same as the highlighting I’ve already done, remember to switch off the collision mask on the card being dragged, so it doesn’t get in the way.)

  • When the user releases the mouse button, if the mouse pointer is over some target card, drop the card that is being dragged onto this target card. Otherwise, put the card being dragged back where it was dragged from. Or alternatively, just drop it at the position it was dragged to. (Just a case of looking to see if the mouse is rolling over any target card when the drag button is released.)

So far, to detect which card the mouse pointer is rolling over or clicking on, I use a CollisionRay from the mouse pointer, which works fine.

Here’s some sample code that implements this.

I found I had to record the difference between the position of the mouse pointer and the position of the object being dragged at the point when the drag button is pressed down, and maintain this gap while the object is being dragged, so the object gets dragged from the point it was clicked on, rather than its center point jumping to the mouse pointer when the drag button is pressed down.

Also I found that when an object is being dragged and you want to detect and highlight which object it is being dragged over, it’s not whether the mouse pointer is over a target object that you care about, but whether the object being dragged is intersecting a target object. In my case this required a rectangle-rectangle intersection test, and it seemed easiest to write my own.

The only bug with this is that if you drag a card and drop it on top of another card, then drag another card over these two, the highlight doesn’t work. It is highlighting the card on the bottom of the pile but not the one on top. Some sort of z-order is needed so that the card on top gets highlighted, or else you need to record which cards currently have some other card on top of them and remove these covered cards from the collisions tests.

import direct.directbase.DirectStart
from pandac.PandaModules import *
from direct.task import Task
import sys

cm = CardMaker('cm')
left,right,bottom,top = 0,2,0,-2
width = right - left
height = top - bottom
cm.setFrame(left,right,bottom,top)

# Colours.
normal = (1,1,1,1)
highlight = (.7,.7,.7,1)

node = aspect2d.attachNewNode('')
node.setPos(-1.2,0,0.9)

cards = []
for i in range(3):        
    for j in range(3):
        card = node.attachNewNode(cm.generate())
        card.setScale(.2)
        card.setPos(i/2.0,0,-j/2.0)
        card.setColor(*normal)
        card.setCollideMask(BitMask32.bit(3))
        cards.append(card)

class DragNDrop:

    def __init__(self):
        base.accept("escape",sys.exit)
        base.accept('mouse1',self.drag)
        base.accept('mouse1-up',self.drop)

        cn = CollisionNode('')
        cn.addSolid(CollisionRay(0,-100,0, 0,1,0))
        cn.setFromCollideMask(BitMask32.bit(3))
        cn.setIntoCollideMask(BitMask32.allOff())
        self.cnp = aspect2d.attachNewNode(cn)
        self.ctrav=CollisionTraverser()
        self.queue = CollisionHandlerQueue()
        self.ctrav.addCollider(self.cnp,self.queue)
        # self.ctrav.showCollisions(aspect2d)
        
        taskMgr.add(self.rolloverTask,'rolloverTask')
        self.rollover = None
        
        self.draggee = None
        self.difference = None

    def rolloverTask(self,t):
        """Move the mouse CollisionRay to the position of the mouse
        pointer, check for collisions, and update self.rollover.
        
        """            
        if not base.mouseWatcherNode.hasMouse():
            return Task.cont        
        
        mpos = base.mouseWatcherNode.getMouse()

        if self.rollover is not None:
            self.rollover.setColor(*normal)
        
        if self.draggee is None:
            # We are not dragging anything, use the CollisionRay to find out if
            # the mouse pointer is over any card.
            position = (mpos[0],0,mpos[1])        
            self.cnp.setPos(render2d,position[0],position[1],position[2])        
            self.ctrav.traverse(aspect2d)                        
            self.queue.sortEntries()
            if self.queue.getNumEntries():	
                self.rollover = self.queue.getEntry(self.queue.getNumEntries()-1).getIntoNodePath()
                self.rollover.setColor(*highlight)
            else:
                self.rollover = None        
        else:
            # We are dragging a card. Do our own test to see if it collides with
            # any other card.
            x,z = mpos[0],mpos[1]
            self.draggee.setPos(render2d,x+self.difference[0],0,z+self.difference[1])
            bl,tr = self.draggee.getTightBounds()
            b,l,t,r = bl.getZ(),bl.getX(),tr.getZ(),tr.getX()
            self.rollover = None
            for card in cards:
                if card == self.draggee: continue
                bl,tr = card.getTightBounds()
                b2,l2,t2,r2 = bl.getZ(),bl.getX(),tr.getZ(),tr.getX()
                if t2 > b and b2 < t and l2 < r and r2 > l: 
                    # Collision
                    self.rollover = card
                    self.rollover.setColor(*highlight)
                    break
        return Task.cont

    def drag(self):
        if self.rollover is not None:
            self.draggee = self.rollover
            self.draggee.setCollideMask(BitMask32.allOff())
            mpos = base.mouseWatcherNode.getMouse()
            self.difference = (self.draggee.getX(render2d)-mpos[0],self.draggee.getZ(render2d)-mpos[1])
        
    def drop(self):
        if self.draggee is not None:
            if self.rollover is not None:
                self.draggee.setPos(self.rollover.getPos())
            self.draggee.setCollideMask(BitMask32.bit(3))
            self.draggee = None
            self.difference = None

d = DragNDrop()
run()            

Hmmm, actually I just realised that in the GNOME desktop anyway, when you’re dragging an object, it’s whether the mouse pointer is over a drop location that counts, not whether the object being dragged intersects a drop location. Implementing it that way makes the code simpler, and gets rid of the highlight bug when you try to successively drop multiple objects on top of each other.

import direct.directbase.DirectStart
from pandac.PandaModules import *
from direct.task import Task
import sys

cm = CardMaker('cm')
left,right,bottom,top = 0,2,0,-2
width = right - left
height = top - bottom
cm.setFrame(left,right,bottom,top)

# Colours.
normal = (1,1,1,1)
highlight = (.7,.7,.7,1)

node = aspect2d.attachNewNode('')
node.setPos(-1.2,0,0.9)

cards = []
for i in range(3):        
    for j in range(3):
        card = node.attachNewNode(cm.generate())
        card.setScale(.2)
        card.setPos(i/2.0,0,-j/2.0)
        card.setColor(*normal)
        card.setCollideMask(BitMask32.bit(3))
        cards.append(card)

class DragNDrop:

    def __init__(self):
        base.accept("escape",sys.exit)
        base.accept('mouse1',self.drag)
        base.accept('mouse1-up',self.drop)

        cn = CollisionNode('')
        cn.addSolid(CollisionRay(0,-100,0, 0,1,0))
        cn.setFromCollideMask(BitMask32.bit(3))
        cn.setIntoCollideMask(BitMask32.allOff())
        self.cnp = aspect2d.attachNewNode(cn)
        self.ctrav=CollisionTraverser()
        self.queue = CollisionHandlerQueue()
        self.ctrav.addCollider(self.cnp,self.queue)
        # self.ctrav.showCollisions(aspect2d)
        
        taskMgr.add(self.rolloverTask,'rolloverTask')
        self.rollover = None
        
        self.draggee = None
        self.difference = None

    def rolloverTask(self,t):
        """Move the mouse CollisionRay to the position of the mouse
        pointer, check for collisions, and update self.rollover.
        
        """            
        if not base.mouseWatcherNode.hasMouse():
            return Task.cont        
        if self.rollover is not None:
            self.rollover.setColor(*normal)
        mpos = base.mouseWatcherNode.getMouse()
        if self.draggee is not None:
            self.draggee.setPos(render2d,mpos[0]+self.difference[0],0,mpos[1]+self.difference[1])                        
        self.cnp.setPos(render2d,mpos[0],0,mpos[1])        
        self.ctrav.traverse(aspect2d)                        
        self.queue.sortEntries()
        if self.queue.getNumEntries():	
            self.rollover = self.queue.getEntry(self.queue.getNumEntries()-1).getIntoNodePath()
            self.rollover.setColor(*highlight)
        else:
            self.rollover = None
            
        return Task.cont

    def drag(self):
        if self.rollover is not None:
            self.draggee = self.rollover
            self.draggee.setCollideMask(BitMask32.allOff())
            mpos = base.mouseWatcherNode.getMouse()
            self.difference = (self.draggee.getX(render2d)-mpos[0],self.draggee.getZ(render2d)-mpos[1])
        
    def drop(self):
        if self.draggee is not None:
            if self.rollover is not None:
                self.draggee.setPos(self.rollover.getPos())
            self.draggee.setCollideMask(BitMask32.bit(3))
            self.draggee = None
            self.difference = None

d = DragNDrop()
run()            

I thought that with all the cards being the same color it might not be obvious what was happening when you drop one card onto another (the position of the card you dropped gets snapped to the position of the card you dropped onto). This version makes the cards different colours. Not sure how much it helps but it looks pretty.

Also, this will not work with DirectGUI objects because even though they are nodepaths their getTightBounds() method doesn’t work. It’s easy to adapt them so that this method does work though, I can provide code for that if anyone wants.

import direct.directbase.DirectStart
from pandac.PandaModules import *
from direct.task import Task
import sys,random

cm = CardMaker('cm')
left,right,bottom,top = 0,2,0,-2
width = right - left
height = top - bottom
cm.setFrame(left,right,bottom,top)

highlight = VBase4(.3,.3,.3,1)

node = aspect2d.attachNewNode('')
node.setPos(-1.2,0,0.9)

cards = []
for i in range(3):        
    for j in range(3):
        card = node.attachNewNode(cm.generate())
        card.setScale(.2)
        card.setPos(i/2.0,0,-j/2.0)
        card.setColor(random.random(),random.random(),random.random())
        card.setCollideMask(BitMask32.bit(3))
        cards.append(card)

class DragNDrop:

    def __init__(self):
        base.accept("escape",sys.exit)
        base.accept('mouse1',self.drag)
        base.accept('mouse1-up',self.drop)

        cn = CollisionNode('')
        cn.addSolid(CollisionRay(0,-100,0, 0,1,0))
        cn.setFromCollideMask(BitMask32.bit(3))
        cn.setIntoCollideMask(BitMask32.allOff())
        self.cnp = aspect2d.attachNewNode(cn)
        self.ctrav=CollisionTraverser()
        self.queue = CollisionHandlerQueue()
        self.ctrav.addCollider(self.cnp,self.queue)
        # self.ctrav.showCollisions(aspect2d)
        
        taskMgr.add(self.rolloverTask,'rolloverTask')
        self.rollover = None
        
        self.draggee = None
        self.difference = None

    def rolloverTask(self,t):
        """Move the mouse CollisionRay to the position of the mouse
        pointer, check for collisions, and update self.rollover.
        
        """            
        if not base.mouseWatcherNode.hasMouse():
            return Task.cont        
        if self.rollover is not None:
            self.rollover.setColor(self.prevColor)
        mpos = base.mouseWatcherNode.getMouse()
        if self.draggee is not None:
            self.draggee.setPos(render2d,mpos[0]+self.difference[0],0,mpos[1]+self.difference[1])                        
        self.cnp.setPos(render2d,mpos[0],0,mpos[1])        
        self.ctrav.traverse(aspect2d)                        
        self.queue.sortEntries()
        if self.queue.getNumEntries():	
            self.rollover = self.queue.getEntry(self.queue.getNumEntries()-1).getIntoNodePath()
            self.prevColor = self.rollover.getColor()
            self.rollover.setColor(self.rollover.getColor()+highlight)
        else:
            self.rollover = None
            
        return Task.cont

    def drag(self):
        if self.rollover is not None:
            self.draggee = self.rollover
            self.draggee.setCollideMask(BitMask32.allOff())
            mpos = base.mouseWatcherNode.getMouse()
            self.difference = (self.draggee.getX(render2d)-mpos[0],self.draggee.getZ(render2d)-mpos[1])
        
    def drop(self):
        if self.draggee is not None:
            if self.rollover is not None:
                self.draggee.setPos(self.rollover.getPos())
            self.draggee.setCollideMask(BitMask32.bit(3))
            self.draggee = None
            self.difference = None

d = DragNDrop()
run()            

I am working on/with a similar system.

however i have one mayor difference, which does make sense if you want to extend the system to different drag&drop behavior. It’s just a bit more complex…

a nodepath uses a python class (for example using the setTag and a object manager)
the python class of the object could look like this, or different if you want different behavior.

class objectClass:
  def __init__( self, parent ):
    self.model = loader.loadModel( 'somemodel.egg' )
    self.parent = parent
    self.model.reparentTo( self.parent )

  def drag( self, newParent ):
    self.model.wrtReparentTo( newParent )
    # if you want to have the object not colliding with the mouse anymore remote the bitmask

  def drop( self ):
    self.model.wrtReparentTo( self.parent )
    #restore the bitmask
  
  def combine( self, object ):
    self.drop()
    object.doCombine() # not like this

  def doCombine( self, otherObject ):
    # what to do when combining?

the mouse handler would look like:

class mouseHandler( directObject ):
  def __init__( self ): 
    self.draggedObject = None
    self.setupCollision()
    self.accept( 'mouse1', self.onPress )
    self.accept( 'mouse1-up', self.onRelease )

  def onPress( self ):
    # drop a object (just failsave, should not happen)
    if self.draggedObject is not None:
      self.draggedObject.drop()
    # get the object you want to drag
    obj = self.mouseRayCollisionCheck()
    # start the dragging
    obj.reparentTo( self.mouseRayNp )

  def onRelease( self ):
    # drop a object (just failsave, should not happen)
    if self.draggedObject is not None:
      self.draggedObject.drop()
    # get the object you want to drag
    obj = self.mouseRayCollisionCheck()
    if obj is not None:
      obj.combine( self.draggedObject )
    self.draggedObject = None

  def mouseRayCollisionCheck( self ):
    # do the collision check
    # get the tag of the collision object
    # find the corresponding class from the objectTag
    return objectClass

this is just example code, it does not run.

Hypnos, in my actual application I’m doing something similar to what I think you’re doing. I have class of objects that own references to nodepaths, and the objects attach themselves to the nodepaths as a python tag.

Do you mean the above example by “your current application”?

Just to sum up my approach, the object themself (or the associated python class) implement the drag&drop functionality. The mouse/collision handler only tells the object that they are clicked/released.

No, I mean the application that I’m developing. The code I posted is just a stand-alone example.

Yeah, I just started developing a protocol just like that myself! :slight_smile:

Here’s an attempt at a new version, it’s more object-oriented and implements the protocol we’ve been talking about. There might be a bug with the highlight colours, I’m not actually sure. I think it’s a bit dodgy that I have two classes Draggable and Droppable that both inherit from a class WrapNP that wraps a NodePath, and then I create NodePaths and wrap them in both a Draggable and a Droppable. There might be some weird collisions going on there.

Also my combining of collision masks so that the same nodepath can be both draggable and droppable isn’t working how I want, so I just make both collision masks the same for now.

import direct.directbase.DirectStart
from pandac.PandaModules import *
from direct.task import Task
import sys,random

# Collision mask worn by all draggable objects.
dragMask = BitMask32.bit(1)
# Collision mask worn by all droppable objects.
dropMask = BitMask32.bit(1)

highlight = VBase4(.3,.3,.3,1)

class WrapNP:
    """Base class for Droppable and Draggable. Wraps a NodePath."""

    def __init__(self,np=None):

        if np is None:
            self._np = aspect2d.attachNewNode('')
        else:
            self._np = np

    def getPos(self,*args):
        return self._np.getPos(*args)

    def highlight(self):
        self._prevColor = self._np.getColor()
        self._np.setColor(self._np.getColor() + VBase4(.3,.3,.3,0))

    def unhighlight(self):
        self._np.setColor(self._prevColor)


class Droppable(WrapNP):
    """An object that can be dropped onto."""

    def __init__(self,np=None):

        WrapNP.__init__(self,np)
        self._np.setPythonTag("droppable",self)
        self._np.setCollideMask(self._np.getCollideMask() | dropMask)

    def drop(self,source):
        """Called by object source when it has been dropped onto this object."""
        
        # Here a Droppable could, for example, transform itself into a StoryCard
        # with the same function as the source StoryCard, and then notify its
        # parent StoryMap.        
        pass

class Draggable(WrapNP):
    """An object that can be dragged."""

    def __init__(self,np):
        
        WrapNP.__init__(self,np)
        
        self._np.setPythonTag("draggable",self)
        self._np.setCollideMask(self._np.getCollideMask() | dropMask)    
            
        # Initialise the CollisionRay that this object uses when it is being
        # dragged to detect if the mouse pointer is over any droppable object.
        cn = CollisionNode('drag collision ray')
        cn.addSolid(CollisionRay(0,-100,0, 0,1,0))
        cn.setFromCollideMask(dropMask)
        cn.setIntoCollideMask(BitMask32.allOff())
        self._cnp = aspect2d.attachNewNode(cn)
        self._ctrav=CollisionTraverser()
        self._queue = CollisionHandlerQueue()
        self._ctrav.addCollider(self._cnp, self._queue)
        # self._ctrav.showCollisions(aspect2d)
        
        # When this object is being dragged this attribute records the 
        # droppable object that the mouse pointer is currently over, if any. 
        self._mouseOver = None

    def drag(self):
        """Called by World when the user starts dragging this object."""

        self._np.setCollideMask(BitMask32.allOff())
        self._dragTask = taskMgr.add(self._drag,'_dragTask')
                    
    def _drag(self,task):
        """Task method used when this node is being dragged. Updates the position 
        of this object to follow the mouse pointer, and does a collision
        check to see if the mouse is over any droppable, stores the result in
        self._mouseOver."""

        if not base.mouseWatcherNode.hasMouse():
            return Task.cont        

        # Move this object and the CollisionRay's NodePath to where the mouse 
        # pointer is.
        mpos = base.mouseWatcherNode.getMouse()
        self._np.setPos(render2d,mpos[0],0,mpos[1])
        self._cnp.setPos(render2d,mpos[0],0,mpos[1])        
        
        # Check whether the CollisionRay hits anything.
        self._ctrav.traverse(aspect2d)   
        if self._mouseOver is not None:
            self._mouseOver.unhighlight()
            self._mouseOver = None
        # If the mouse is not over a DirectGUI object and the CollisionRay does 
        # collide with some NodePath, then find the corresponding Droppable.
        if not base.mouseWatcherNode.isOverRegion() \
        and self._queue.getNumEntries():
            # Find the nearest containing Droppable from the first NodePath
            # that the ray collided with.
            self._queue.sortEntries()	
            np = self._queue.getEntry(self._queue.getNumEntries()-1).getIntoNodePath()
            while np != aspect2d:
                if np.hasPythonTag('droppable'):
                    self._mouseOver = np.getPythonTag('droppable')
                    self._mouseOver.highlight()
                    break
                np = np.getParent()
                
        return Task.cont
        
    def drop(self):
        """Called by World when the user drops this object."""
        
        self._np.setCollideMask(self._np.getCollideMask() | dropMask)
        taskMgr.remove(self._dragTask)
        
        if self._mouseOver is None:
            # Drop this object where it is? Return it to the position it was
            # dragged from?
            pass
        else:
            # Drop this object on self._mouseOver.
            self._np.setPos(self._mouseOver.getPos())
            # self.parent.removed(self)
            self._mouseOver.drop(self)
          
class World:

    def __init__(self):
        base.accept("escape",sys.exit)
        base.accept('mouse1',self._drag)
        base.accept('mouse1-up',self._drop)

        # Initialise the collision ray that is used to detect which draggable
        # object the mouse pointer is over.    
        cn = CollisionNode('')
        cn.addSolid(CollisionRay(0,-100,0, 0,1,0))
        cn.setFromCollideMask(dragMask)
        cn.setIntoCollideMask(BitMask32.allOff())
        self.cnp = aspect2d.attachNewNode(cn)
        self.ctrav = CollisionTraverser()
        self.queue = CollisionHandlerQueue()
        self.ctrav.addCollider(self.cnp,self.queue)
        # self.ctrav.showCollisions(aspect2d)
        
        taskMgr.add(self._mouseOverTask,'_mouseOverTask')
        self._mouseOver = None

        self._draggee = None

    def _mouseOverTask(self,task):

        if self._draggee is not None:
            return Task.cont
        
        if not base.mouseWatcherNode.hasMouse():
            return Task.cont        
        
        if self._mouseOver is not None:
            self._mouseOver.unhighlight()
        
        mpos = base.mouseWatcherNode.getMouse()
        self.cnp.setPos(render2d,mpos[0],0,mpos[1])        
        self.ctrav.traverse(aspect2d)                        
        self.queue.sortEntries()
        if self.queue.getNumEntries():	
            np = self.queue.getEntry(self.queue.getNumEntries()-1).getIntoNodePath()           
            while np != aspect2d:
                if np.hasPythonTag('draggable'):
                    self._mouseOver = np.getPythonTag('draggable')
                    break
                np = np.getParent()
            self._mouseOver.highlight()
        else:
            self._mouseOver = None
            
        return Task.cont
    
    def _drag(self):
        if self._mouseOver is not None:
            self._draggee = self._mouseOver
            self._draggee.drag()
        
    def _drop(self):
        if self._draggee is not None:
            self._draggee.drop()
            self._draggee = None
            
cm = CardMaker('cm')
left,right,bottom,top = 0,2,0,-2
width = right - left
height = top - bottom
cm.setFrame(left,right,bottom,top)
node = aspect2d.attachNewNode('')
node.setPos(-1.2,0,0.9)
cards = []
for i in range(3):        
    for j in range(3):
        card = node.attachNewNode(cm.generate())
        card.setScale(.2)
        card.setPos(i/2.0,0,-j/2.0)
        card.setColor(random.random(),random.random(),random.random())
        cards.append(card)
        draggable = Draggable(card)
        droppable = Droppable(card)

w = World()
run()

i have put together a sample how i would make it:

it’s currently missing highlighting, but that’s easy to add. also creating a different type of drag/drop object would be quite easy. You may want to change the way the collidemask off method works (save the old collide mask and apply it on release again)

import direct.directbase.DirectStart
from pandac.PandaModules import *
from direct.task import Task
import sys,random

# Collision mask worn by all draggable objects.
dragMask = BitMask32.bit(1)
# Collision mask worn by all droppable objects.
dropMask = BitMask32.bit(1)

highlight = VBase4(.3,.3,.3,1)

class objectMangerClass:
  def __init__( self ):
    self.objectIdCounter = 0
    self.objectDict = dict()
  
  def tag( self, objectNp, objectClass ):
    self.objectIdCounter += 1
    objectTag = str(self.objectIdCounter)
    objectNp.setTag( 'objectId', objectTag )
    self.objectDict[objectTag] = objectClass
  
  def get( self, objectTag ):
    if objectTag in self.objectDict:
      return self.objectDict[objectTag]
    return None
objectManger = objectMangerClass()

class dragDropObjectClass:
  def __init__( self, np ):
    self.model = np
    self.previousParent = None
    self.model.setCollideMask(dragMask)
    objectManger.tag( self.model, self )
  
  def onPress( self, mouseNp ):
    self.previousParent = self.model.getParent()
    self.model.wrtReparentTo( mouseNp )
    self.model.setCollideMask(BitMask32.allOff())
  
  def onRelease( self ):
    self.model.wrtReparentTo( self.previousParent )
    self.model.setCollideMask(dragMask)
  
  def onCombine( self, otherObj ):
    self.model.setPos( otherObj.model.getPos() )

class mouseCollisionClass:
  def __init__(self):
    base.accept("escape",sys.exit)
    base.accept('mouse1',self.onPress)
    base.accept('mouse1-up',self.onRelease)
    self.draggedObj = None
    self.setupCollision()
    
    taskMgr.add( self.mouseMoverTask, 'mouseMoverTask' )

  def setupCollision( self ):
    # Initialise the collision ray that is used to detect which draggable
    # object the mouse pointer is over.   
    cn = CollisionNode('')
    cn.addSolid( CollisionRay(0,-100,0, 0,1,0) )
    cn.setFromCollideMask(dragMask)
    cn.setIntoCollideMask(BitMask32.allOff())
    self.cnp = aspect2d.attachNewNode(cn)
    self.ctrav = CollisionTraverser()
    self.queue = CollisionHandlerQueue()
    self.ctrav.addCollider(self.cnp,self.queue)
    self.cnp.show()
  
  def mouseMoverTask( self, task ):
    if base.mouseWatcherNode.hasMouse():
      mpos = base.mouseWatcherNode.getMouse()
      self.cnp.setPos(render2d,mpos[0],0,mpos[1])       
    return task.cont
  
  def collisionCheck( self ):
    self.ctrav.traverse(aspect2d)                       
    self.queue.sortEntries()
    if self.queue.getNumEntries():
      np = self.queue.getEntry( self.queue.getNumEntries()-1 ).getIntoNodePath() #self.queue.getNumEntries()-1
      objectId = np.getTag( 'objectId' )
      if objectId is None:
        objectId = np.findNetTag( 'objectId' )
      if objectId is not None:
        object = objectManger.get( objectId )
        return object
    return None
  
  def onPress( self ):
    obj = self.collisionCheck()
    if obj is not None:
      self.draggedObj = obj
      obj.onPress( self.cnp )
  
  def onRelease( self ):
    obj = self.collisionCheck()
    self.draggedObj.onRelease() # self.cnp )
    if obj is not None:
      self.draggedObj.onCombine( obj )


if __name__ == '__main__':
  cm = CardMaker('cm')
  left,right,bottom,top = 0,2,0,-2
  width = right - left
  height = top - bottom
  cm.setFrame(left,right,bottom,top)
  node = aspect2d.attachNewNode('')
  node.setPos(-1.2,0,0.9)
  cards = []
  
  for i in range(3):       
    for j in range(3):
      card = node.attachNewNode(cm.generate())
      card.setScale(.2)
      card.setPos(i/2.0,0,-j/2.0)
      card.setColor(random.random(),random.random(),random.random())
      draggable = dragDropObjectClass(card)
      cards.append(card)
  
  mouseCollision = mouseCollisionClass()
  run()

I like the way you reparent the draggable to a mouse nodepath when it’s being dragged, so that it moves automatically instead of having to move itself. When integrating my version into my larger application, I found the object being dragged would appear underneath other objects, so I had to reparent it to aspect2d to make it appear on top. But there’s something nice about having a mouse nodepath that just follows the mouse around and is always drawn last (so always ‘on top’), and just attaching things that are dragged to that.

I think if all your dragDropObjectClass instances are going to register themselves with the objectMangerClass on initialisation, they also need to unregister themselves on deletion. (The del method I think.) I’m not sure I totally understand what objectMangerClass is for.

The objectManager keeps the references from the nodepath tag’s to the associated object’s class. You can actually use the setPythonTag instead. However i got used to use a objectManager.

you can somehow define the order of the nodepaths if set into aspect2d. Usually it’s the order you create the things. So if you create the mouseray earlier it might be above the other things. (not sure)

Yeah you can pass an option ‘sort’ to reparentTo to control the order of a new child node relavent to other child nodes. I think if you don’t pass sort it gets added as the last child. My guess is that Panda3D does a left to right, depth first traversal of the scene graph to render it. So the last thing to be rendered will be the rightmost node of all, and if you attach a new node to the root node, and that node has no children of its own, then it becomes the rightmost node of all and will be rendered last (and therefore on top of everything else). It seems to work anyway.

Right. You can also override the draw order by putting an object into an explicit bit. For instance:
object.setBin(‘gui-popup’, 0)
will make it render on top of most things, without having to reparent it, since the gui-popup bin (which is defined in the Config.prc file) is set up to render after the default bins.

David

if you use a 3d ray collision drag&drop function, the method i am using makes even more sense, because if the mouse is hidden&centered you can drag&drop stuff using the same technique as if you have a mouse which is moving freely in the window.

drwr, is the left-to-right depth-first render order a property we can rely on? I have been using it up until now with my hierarchy of nodes beneath aspect2d. But then I inserted a new node beneath aspect2d and above the rest of my hierarchy, and the render order suddenly got all messed up, and the render order seems to change as transformations are applied to this new node. See my last post in this thread.

Cull bins don’t really seem appropriate to my application, which has a complex hierarchical structure of nodes in the 2D scene graph. It is best if Panda simply renders the nodes the way they are arranged in the scene graph, so child nodes are drawn on top of parent nodes, which was how it was working until now. I don’t understand why this change has broken that property.

Question answered in the linked thread.

David