Picking a LineSegs

Hey all, I need to be able to pick a LineSegs in 3D space. I’ve experimented for a while with CollisionTubes to get it done, but have no clue how to get that to work. Pointers? Thanks!

For illustration purposes, this is the code that draws the LineSegs:

def _draw_line_segs(self, parent, points, thickness=1):
    """Draw the given line segments."""

    segs = LineSegs()
    segs.setThickness(thickness)

    if points:
        segs.moveTo(points[0][0], points[0][1], points[0][2])
        for p in points[1:]:
            segs.drawTo(p[0], p[1], p[2])

    lines = NodePath(segs.create())
    lines.reparentTo(parent)
    return lines

They look fine and all, but there is no way to click on them :S I’m looking for any ideas at this point, since I’ve tried the one or two I’ve thought of. Thanks!

Line segments are not clickable because they have no width. (Even if you use setThickness() to specify a pixel width, that doesn’t count as a width in 3-D space, because it’s only a rendering trick in pixels.)

If you really need to click on your line segments, then you need to create actual collision geometry that has the same shape. One possibility is to put a single CollisionSphere around the whole drawing. If you really need to click on the precise shape, though, then you will need to build a construction of many CollisionTubes or CollisionPolygons that follow the same path as your line segments.

David

Aha so CollisionTubes were the way to go. When I created them along the same area as a wire, though, they were never picked by my picking code.

hx, hy, hz = here.getX(), here.getY(), here.getZ()
tx, ty, tz = there.getX(), there.getY(), there.getZ()
tubeRadius = 0.6
tube = CollisionTube(hx, hy, hz, tx, ty, tz, tubeRadius)
tubeNp = parent.attachNewNode(CollisionNode('wireCollisionTube'))
tubeNp.node().addSolid(tube)
tubeNp.setTag('pickable', 'true') # EDIT
tubeNp.show()

Well, I don’t see a problem with that code.

Here is the picking code. When I click on a CollisionTube (which is easy because I told them to be visible), the if statement “if not self.queue.getNumEntries()” triggers and everything comes to a stop. It’s like the tubes aren’t there at all.

class Picker(object):

    def __init__(self):
        self.queue = CollisionHandlerQueue()
        self.pickerNode = CollisionNode('MouseRay')
        self.pickerNP = base.camera.attachNewNode(self.pickerNode)
        self.pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask())
        self.pickerRay = CollisionRay()
        self.pickerNode.addSolid(self.pickerRay)
        self.traverser = CollisionTraverser()
        self.traverser.addCollider(self.pickerNP, self.queue)

    def __call__(self, *args, **kwargs):
        mpos = kwargs.get('mpos', None if not args else args[0])
        return self.get_object_hit(mpos)

    def get_object_hit(self, mpos=None):
        if mpos is None:
            if not base.mouseWatcherNode.hasMouse():
                return None
            mpos = base.mouseWatcherNode.getMouse()

        self.pickerRay.setFromLens(base.camNode, mpos.getX(), mpos.getY())
        self.traverser.traverse(render)

        if not self.queue.getNumEntries():
            # THIS HAPPENS FOR COLLISIONTUBES
            return None

        self.queue.sortEntries()
        np = self.queue.getEntry(0).getIntoNodePath()
        while np != render:
            if np.getTag('pickable') == 'true' and not np.isHidden():
                return np
            np = np.getParent()
        return None

This line:

self.pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask()) 

tells your CollisionRay to look only for visible geometry (GeomNodes), not actual collision geometry. Remove it.

David

The line being removed does not seem to help in this case, but thanks for pointing it out.

Well, then, the next thing to do is the standard advice when your collisions don’t seem to be firing: double-check that your collide bitmasks are indeed set correctly so that your picker node has at least one bit in common with the node that you intend to collide with. Double-check that the picker node is in the space coordinate space and is, in fact, intersecting with the node you intend to collide with.

David

You sound slightly annoyed with my question, and I apologize if I haven’t worked with the underlying work done by collisions before. Thanks to your tip, though, I read up on collision masks. After printing out the mask the CollisionNodes had, It was obvious why the CollisionRay wasn’t noticing them:

>>> print CollisionNode('cNode').getIntoCollideMask()
 0000 0000 0000 1111 1111 1111 1111 1111
>>> print GeomNode.getDefaultCollideMask()
 0000 0000 0001 0000 0000 0000 0000 0000

At this point I had a choice: set the bitmask of my CollisionNode or don’t set them. I went with setting them to a custom bitmask with just bit 12 (completely arbitrary btw) on, so that if I need to distinguish between types of collisions later on, it would be easier.

So now my code that creates the CollisionNode and then the CollisionWire is as follows:

# draw the wire (assumes code from previous posts in this thread)
lineSegs = self._draw_line_segs(parent, (here, there))
lineSegs.setTag('pickable', 'true')

# add a tube around the wire so it can be picked
hx, hy, hz = here.getX(), here.getY(), here.getZ()
tx, ty, tz = there.getX(), there.getY(), there.getZ()
tubeRadius = 0.6
tube = CollisionTube(hx, hy, hz, tx, ty, tz, tubeRadius)
cNode = CollisionNode('wireCollisionTube')
cNode.setIntoCollideMask(BitMask32.bit(12))
tubeNp = parent.attachNewNode(cNode)
tubeNp.node().addSolid(tube)
tubeNp.show()  # optional, just for debugging

Note the new line that sets the “into” collision mask. Oh by the way, at first, led astray by the “self.pickerNode.setFromCollideMask” line in the Picker class, I tried setting the “from” collision mask on my CollisionNode. That doesn’t work, but does as soon as the “into” mask is used instead.

Okay, the the Picker class remains largely the same, but for posterity here it is anyhow. I can’t bold-face the changes inside a code tag, but I’ve added comments at the appropriate places.

class Picker(object):

    def __init__(self):
        # CHANGED: combine all required masks
        mask = GeomNode.getDefaultCollideMask()
        mask |= BitMask32.bit(12)

        self.queue = CollisionHandlerQueue()
        self.pickerNode = CollisionNode('MouseRay')
        self.pickerNP = base.camera.attachNewNode(self.pickerNode)
        # CHANGED: set "from" collision mask to the new combined version
        self.pickerNode.setFromCollideMask(mask)
        self.pickerRay = CollisionRay()
        self.pickerNode.addSolid(self.pickerRay)
        self.traverser = CollisionTraverser()
        self.traverser.addCollider(self.pickerNP, self.queue)

    def __call__(self, *args, **kwargs):
        mpos = kwargs.get('mpos', None if not args else args[0])
        return self.get_object_hit(mpos)

    def get_object_hit(self, mpos=None):
        if mpos is None:
            if not base.mouseWatcherNode.hasMouse():
                return None
            mpos = base.mouseWatcherNode.getMouse()

        self.pickerRay.setFromLens(base.camNode, mpos.getX(), mpos.getY())
        self.traverser.traverse(render)

        if not self.queue.getNumEntries():
            return None

        self.queue.sortEntries()
        np = self.queue.getEntry(0).getIntoNodePath()
        while np != render:
            if np.getTag('pickable') == 'true' and not np.isHidden():
            return np
            np = np.getParent()
        return None

Useful links:

Thanks as usual, David!