Ok from now on I’ll take a closer look into the manual before posting such silly mistakes. Guess I was fixed to my example scripts and didn’t think of the Bitmask manual page.
I must have overlooked the missing getParent() because the interpreter never reached that part 
And again: Many thanks for the help ynjh_jo!
I’ve updated the snippet with the fixes and CONTROL + SHIFT key support for adding / removing selected entries. Might be useful for others too
from pandac.PandaModules import *
from direct.directbase.DirectStart import *
from direct.showbase.DirectObject import DirectObject
from direct.interval.IntervalGlobal import *
from direct.task import Task
import random, sys
class ClickSelector(DirectObject):
def __init__(self):
self.collTrav = CollisionTraverser('selector')
self.selHandler = CollisionHandlerQueue()
self.selection = set()
self.tmpSelection = set()
CM = CardMaker('sel')
CM.setFrame(0, 1, 0, 1)
self.rect = render2d.attachNewNode(CM.generate())
self.rect.setColor(0, 1, 1, .2)
self.rect.setTransparency(1)
self.rect.hide()
LS = LineSegs()
LS.moveTo(0, 0, 0)
LS.drawTo(1, 0, 0)
LS.drawTo(1, 0, 1)
LS.drawTo(0, 0, 1)
LS.drawTo(0, 0, 0)
self.rect.attachNewNode(LS.create())
self.accept("control-mouse1", self.click)
self.accept("shift-mouse1", self.click)
self.accept("mouse1", self.click)
# add collision-detection for single clicks
self.clickColNP = camera.attachNewNode(CollisionNode('sel'))
self.clickColRay = CollisionRay()
self.clickColNP.node().addSolid(self.clickColRay)
# set the FROM mask to 0x10 to match the INTO mask of the balls
self.clickColNP.node().setFromCollideMask(BitMask32.bit(2))
self.clickColNP.node().setIntoCollideMask(BitMask32.allOff())
self.collTrav.addCollider(self.clickColNP, self.selHandler)
def addSelectable(self, obj):
self.collTrav.addCollider(obj, self.selHandler)
def click(self):
if not base.mouseWatcherNode.hasMouse():
return
self.click_start = Point2(base.mouseWatcherNode.getMouse())
self.rect.setPos(self.click_start[0], 1, self.click_start[1])
self.rect.show()
t = taskMgr.add(self.update_rect, "update_rect")
self.acceptOnce("mouse1-up", self.release_click, extraArgs=[t])
def update_rect(self, task):
if not base.mouseWatcherNode.hasMouse(): # check for mouse first, in case the mouse is outside the Panda window
return Task.cont
d = base.mouseWatcherNode.getMouse() - self.click_start
self.rect.setScale(d[0] if d[0] else 1e-3, 1, d[1] if d[1] else 1e-3)
return task.cont
def release_click(self, t):
taskMgr.remove(t)
if not base.mouseWatcherNode.hasMouse():
return
curMousePos = Point2(base.mouseWatcherNode.getMouse())
# check for single clicks
if curMousePos >= self.click_start - 0.01 and curMousePos <= self.click_start + 0.01:
self.rect.hide()
self.handleMouseClick()
else:
self.handleMouseDrag()
# clear collision result
self.selHandler.clearEntries()
# check for pressed shift or control keys
shiftKey = base.mouseWatcherNode.isButtonDown(KeyboardButton.shift())
controlKey = base.mouseWatcherNode.isButtonDown(KeyboardButton.control())
# now update the selection
# SHIFT: add to the old selection
# CONTROL: remove from the old selection
# NO-KEY: set as new selection
newSet = set()
for i in self.selection:
if shiftKey:
newSet.add(i)
self.selectObj(i)
elif controlKey:
if not i in self.tmpSelection:
newSet.add(i)
self.selectObj(i)
else:
self.deselectObj(i)
else:
self.deselectObj(i)
if not controlKey:
for i in self.tmpSelection:
newSet.add(i)
self.selectObj(i)
self.selection = newSet
self.tmpSelection = set()
def selectObj(self, obj):
obj.setColorScale(Vec4(1, 0, 0, 1))
def deselectObj(self, obj):
obj.setColorScale(Vec4(1))
def handleMouseClick(self):
self.clickColRay.setFromLens(base.camNode, self.click_start[0], self.click_start[1])
# start collision check
self.collTrav.traverse(render)
# check for collisions
if self.selHandler.getNumEntries() > 0:
# sort to select the front-most entry
self.selHandler.sortEntries()
n = self.selHandler.getEntry(0).getIntoNodePath().getParent()
self.tmpSelection.add(n)
def handleMouseDrag(self):
bmin, bmax = self.rect.getTightBounds()
clickLL = Point2(bmin[0], bmin[2]) # lower left
clickUR = Point2(bmax[0], bmax[2]) # upper right
if clickUR == clickLL: # Fudge the numbers a bit to avoid the degenerate case of no rectangle
clickUR = Point2(clickUR[0] + .00001, clickUR[1] + .00001)
crap = Point3()
llF = Point3()
urF = Point3()
base.camLens.extrude(clickLL, crap, llF)
base.camLens.extrude(clickUR, crap, urF)
ulF = Point3(llF[0], llF[1], urF[2])
brF = Point3(urF[0], urF[1], llF[2])
camOrigin = Point3(0)
left = CollisionPlane(Plane(camOrigin, ulF, llF)) # Create the 4 sides with planes
right = CollisionPlane(Plane(camOrigin, brF, urF)) # They should all 'face' OUTward: i.e.
bot = CollisionPlane(Plane(camOrigin, llF, brF)) # Collisions are INSIDE
top = CollisionPlane(Plane(camOrigin, urF, ulF))
pyramid = camera.attachNewNode(CollisionNode('pyramid'))
pyramid.node().addSolid(left)
pyramid.node().addSolid(top)
pyramid.node().addSolid(right)
pyramid.node().addSolid(bot)
# check for collisions
self.collTrav.traverse(render)
if self.selHandler.getNumEntries() > 0:
hits = []
for i in range(self.selHandler.getNumEntries()):
hits.append(self.selHandler.getEntry(i).getFromNodePath().getParent())
# If the object is within the rectangle, it collides with all 4 planes.
# Use set to remove duplicates collision entries of the same object.
self.tmpSelection = set(filter(lambda i: hits.count(i) == 4, hits))
pyramid.removeNode()
self.rect.hide()
class World(DirectObject):
def __init__(self):
self.selector = ClickSelector()
for i in range(20):
ball = loader.loadModel("smiley")
ball.reparentTo(render)
ball.setPos(random.randrange(-3.0, 3.0) * 2, random.randrange(-3.0, 3.0) * 2, random.randrange(-3.0, 3.0) * 2)
# set the collision sphere to collide FROM the drag-Planes and INTO the click-Ray
cnodePath = ball.attachCollisionSphere(
'sel', 0, 0, 0, 1, fromCollide=BitMask32.bit(1), intoCollide=BitMask32.bit(2))
self.selector.addSelectable(cnodePath)
# ~ ball.showCS()
self.accept("escape", sys.exit)
base.disableMouse()
# animate camera orientation, to see if the pyramid is correct
camPivot = render.attachNewNode('')
camera.reparentTo(camPivot)
camera.setY(-30)
pos = camera.getPos()
Sequence(
camera.posInterval(20, pos * 1.5, pos),
camera.posInterval(20, pos),
).loop()
camPivot.hprInterval(150, Vec3(360)).loop()
winst = World()
run()