示例#1
0
class Character:
    def __init__(self, name, model):
        self.name = name
        self.node = NodePath(name + "_character")
        self.node.reparent_to(render)
        self.model = model
        self.model.reparent_to(self.node)
        self.traverser = CollisionTraverser()
        self.item_ray = self.ray("item", base.itemmask, point_b=(0, 1, 1))
        self.movement = Vec3(0, 0, 0)
        self.speed = 0.85
        self.keys = ""

    def ray(self, name, bitmask, point_a=(0, 0, 1), point_b=(0, 0, 0)):
        shape = CollisionSegment(point_a, point_b)
        col = CollisionNode(self.node.getName() + "-ray-" + name)
        col.add_solid(shape)
        col.set_from_collide_mask(bitmask)
        col.set_into_collide_mask(CollideMask.all_off())
        col_node = self.node.attach_new_node(col)
        handler = CollisionHandlerQueue()
        self.traverser.add_collider(col_node, handler)
        return {
            "collider": col,
            "shape": shape,
            "handler": handler,
            "node": col_node
        }

    def sphere(self, name, bitmask, pos=(0, 0, 1), radius=0.2):
        col = CollisionNode(self.node.getName() + "-sphere-" + name)
        shape = CollisionSphere(pos, radius)
        col.add_solid(shape)
        col.set_from_collide_mask(bitmask)
        col.set_into_collide_mask(CollideMask.allOff())
        col_node = self.node.attachNewNode(col)
        handler = CollisionHandlerPusher()
        handler.add_collider(col_node, self.node)
        self.traverser.add_collider(col_node, handler)
        return {
            "collider": col,
            "shape": shape,
            "handler": handler,
            "node": col_node
        }

    def fall(self):
        if self.fall_ray["handler"].get_num_entries() > 0:
            self.fall_ray["handler"].sort_entries()
            closest_entry = list(self.fall_ray["handler"].entries)[0]
            collision_position = closest_entry.get_surface_point(render)
            self.node.set_z(collision_position.get_z())
            self.movement.z = 0
        else:
            self.movement.z -= base.dt

    def open_doors(self):
        if self.item_ray["handler"].get_num_entries() > 0:
            closest_entry = list(self.item_ray["handler"].entries)[0]
            item = closest_entry.get_into_node_path()
            if item.name[0] == "d":
                door = base.map.doors[item.name]
                if not door.lock or door.lock in self.keys:
                    door.open = True
示例#2
0
class Transform(Component):
    """Each game object has exactly one of these. A transform holds data
    about position, rotation, scale and parent relationship.
    
    In Panity this is a wrapper for a NodePath.
    """
    def __init__(self, game_object, name):
        # Component class sets self.game_object = game_object
        Component.__init__(self, game_object)
        self.node = NodePath(name)
        self.node.setPythonTag("transform", self)

    @classmethod
    def getClassSerializedProperties(cls):
        """Return all special property attributes in a dict. Only attributes
        derived from SerializedProperty are respected.

        On transform component this method always returns local position,
        -rotation and scale only.
        """
        d = {}
        d["local_position"] = Transform.local_position
        d["local_euler_angles"] = Transform.local_euler_angles
        d["local_scale"] = Transform.local_scale
        return d

    def getSerializedProperties(self):
        """Return all properties for serialization. In the case of transform
        this only returns local position, -rotation and -scale, which are
        required to restore the state of the node.
        """
        d = {}
        d["local_position"] = self.local_position
        d["local_euler_angles"] = self.local_euler_angles
        d["local_scale"] = self.local_scale
        return d

    # We use the panda node to save the name on it for better debugging and
    # efficient finding of nodes with NodePath().find()
    @SerializedPropertyDecorator
    def name(self):
        return self.node.getName()

    @name.setter
    def name(self, name):
        if name == "render":
            name = "_render"
        self.node.setName(name)

    @SerializedPropertyDecorator
    def position(self):
        return self.node.getPos(self.root.node)

    @position.setter
    def position(self, position):
        self.node.setPos(self.root.node, *position)

    @SerializedPropertyDecorator
    def local_position(self):
        return self.node.getPos()

    @local_position.setter
    def local_position(self, position):
        self.node.setPos(*position)

    @SerializedPropertyDecorator
    def euler_angles(self):
        return self.node.getHpr(self.root.node)

    @euler_angles.setter
    def euler_angles(self, angles):
        self.node.setHpr(self.root.node, *angles)

    @SerializedPropertyDecorator
    def local_euler_angles(self):
        return self.node.getHpr()

    @local_euler_angles.setter
    def local_euler_angles(self, angles):
        self.node.setHpr(*angles)

    @SerializedPropertyDecorator
    def rotation(self):
        return self.node.getQuat(self.root.node)

    @rotation.setter
    def rotation(self, quaternion):
        self.node.setQuat(self.root.node, *quaternion)

    @SerializedPropertyDecorator
    def local_rotation(self):
        return self.node.getQuat()

    @local_rotation.setter
    def local_rotation(self, quaternion):
        self.node.setQuat(*quaternion)

    @SerializedPropertyDecorator
    def local_scale(self):
        return self.node.getScale()

    @local_scale.setter
    def local_scale(self, scale):
        self.node.setScale(*scale)

    @SerializedPropertyDecorator
    def parent(self):
        p = self.node.getParent()
        if p.isEmpty() or p.getName() == "render":
            return self
        elif p.hasPythonTag("transform"):
            return p.getPythonTag("transform")

    @parent.setter
    def parent(self, parent):
        self.node.wrtReparentTo(parent.node)

    @SerializedPropertyDecorator
    def root(self):
        if self.parent is not self:
            return self.parent.root()
        else:
            return self

    def destroy(self):
        """Ultimately remove this transform. Warning: this might cause errors
        for other components on this game object. Use this only when removing
        the whole GameObject.
        """
        self.node.removeNode()

    def getChildren(self):
        """Return children as Transforms."""
        # this requires the __iter__() method
        return [c for c in self]

    def __iter__(self):
        """Iterate over children nodes and yield the transform instances."""
        for child in self.node.getChildren():
            if child.hasPythonTag("transform"):
                yield child.getPythonTag("transform")

    def __str__(self):
        r = "Transform for '{}'\n".format(self.name)
        r += "local position: {}\n".format(self.local_position)
        r += "local rotation: {}\n".format(self.local_euler_angles)
        r += "local scale:    {}\n".format(self.local_scale)
        return r
示例#3
0
文件: transform.py 项目: Moqi/panity
class Transform(Component):
    """Each game object has exactly one of these. A transform holds data
    about position, rotation, scale and parent relationship.
    
    In Panity this is a wrapper for a NodePath.
    """

    def __init__(self, game_object, name):
        # Component class sets self.game_object = game_object
        Component.__init__(self, game_object)
        self.node = NodePath(name)
        self.node.setPythonTag("transform", self)

    @classmethod
    def getClassSerializedProperties(cls):
        """Return all special property attributes in a dict. Only attributes
        derived from SerializedProperty are respected.

        On transform component this method always returns local position,
        -rotation and scale only.
        """
        d = {}
        d["local_position"] = Transform.local_position
        d["local_euler_angles"] = Transform.local_euler_angles
        d["local_scale"] = Transform.local_scale
        return d

    def getSerializedProperties(self):
        """Return all properties for serialization. In the case of transform
        this only returns local position, -rotation and -scale, which are
        required to restore the state of the node.
        """
        d = {}
        d["local_position"] = self.local_position
        d["local_euler_angles"] = self.local_euler_angles
        d["local_scale"] = self.local_scale
        return d

    # We use the panda node to save the name on it for better debugging and
    # efficient finding of nodes with NodePath().find()
    @SerializedPropertyDecorator
    def name(self):
        return self.node.getName()
    @name.setter
    def name(self, name):
        if name == "render":
            name = "_render"
        self.node.setName(name)

    @SerializedPropertyDecorator
    def position(self):
        return self.node.getPos(self.root.node)
    @position.setter
    def position(self, position):
        self.node.setPos(self.root.node, *position)
    
    @SerializedPropertyDecorator
    def local_position(self):
        return self.node.getPos()
    @local_position.setter
    def local_position(self, position):
        self.node.setPos(*position)

    @SerializedPropertyDecorator
    def euler_angles(self):
        return self.node.getHpr(self.root.node)
    @euler_angles.setter
    def euler_angles(self, angles):
        self.node.setHpr(self.root.node, *angles)
    
    @SerializedPropertyDecorator
    def local_euler_angles(self):
        return self.node.getHpr()
    @local_euler_angles.setter
    def local_euler_angles(self, angles):
        self.node.setHpr(*angles)
    
    @SerializedPropertyDecorator
    def rotation(self):
        return self.node.getQuat(self.root.node)
    @rotation.setter
    def rotation(self, quaternion):
        self.node.setQuat(self.root.node, *quaternion)

    @SerializedPropertyDecorator
    def local_rotation(self):
        return self.node.getQuat()
    @local_rotation.setter
    def local_rotation(self, quaternion):
        self.node.setQuat(*quaternion)

    @SerializedPropertyDecorator
    def local_scale(self):
        return self.node.getScale()
    @local_scale.setter
    def local_scale(self, scale):
        self.node.setScale(*scale)

    @SerializedPropertyDecorator
    def parent(self):
        p = self.node.getParent()
        if p.isEmpty() or p.getName() == "render":
            return self
        elif p.hasPythonTag("transform"):
            return p.getPythonTag("transform")
    @parent.setter
    def parent(self, parent):
        self.node.wrtReparentTo(parent.node)

    @SerializedPropertyDecorator
    def root(self):
        if self.parent is not self:
            return self.parent.root()
        else:
            return self
    
    def destroy(self):
        """Ultimately remove this transform. Warning: this might cause errors
        for other components on this game object. Use this only when removing
        the whole GameObject.
        """
        self.node.removeNode()

    def getChildren(self):
        """Return children as Transforms."""
        # this requires the __iter__() method
        return [c for c in self]

    def __iter__(self):
        """Iterate over children nodes and yield the transform instances."""
        for child in self.node.getChildren():
            if child.hasPythonTag("transform"):
                yield child.getPythonTag("transform")
    
    def __str__(self):
        r = "Transform for '{}'\n".format(self.name)
        r += "local position: {}\n".format(self.local_position)
        r += "local rotation: {}\n".format(self.local_euler_angles)
        r += "local scale:    {}\n".format(self.local_scale)
        return r
  def setupPhysics(self):

    # setting up physics world and parent node path 
    self.physics_world_ = BulletWorld()
    self.world_node_ = self.render.attachNewNode('world')
    self.cam.reparentTo(self.world_node_)
    self.physics_world_.setGravity(Vec3(0, 0, -9.81))

    self.debug_node_ = self.world_node_.attachNewNode(BulletDebugNode('Debug'))
    self.debug_node_.node().showWireframe(True)
    self.debug_node_.node().showConstraints(True)
    self.debug_node_.node().showBoundingBoxes(True)
    self.debug_node_.node().showNormals(True)
    self.physics_world_.setDebugNode(self.debug_node_.node())
    self.debug_node_.hide()

    # setting up ground
    self.ground_ = self.world_node_.attachNewNode(BulletRigidBodyNode('Ground'))
    self.ground_.node().addShape(BulletPlaneShape(Vec3(0, 0, 1), 0))
    self.ground_.setPos(0,0,0)
    self.ground_.setCollideMask(BitMask32.allOn())
    self.physics_world_.attachRigidBody(self.ground_.node())

    self.object_nodes_ = []
    self.controlled_objects_=[]
    num_boxes = 20
    side_length = 0.2
    size = Vec3(0.5*side_length,0.5*side_length,0.5*side_length)
    start_pos = Vec3(-num_boxes*side_length,0,6)

    # creating boxes
    box_visual = loader.loadModel('models/box.egg')
    box_visual.clearModelNodes()
    box_visual.setTexture(loader.loadTexture('models/wood.png'))
    #box_visual.setRenderModeWireframe()
    bounds = box_visual.getTightBounds() # start of box model scaling
    extents = Vec3(bounds[1] - bounds[0])
    scale_factor = side_length/max([extents.getX(),extents.getY(),extents.getZ()])
    box_visual.setScale((scale_factor,scale_factor ,scale_factor)) # end of box model scaling

    for i in range(0,20):
      self.addBox("name %i"%(i),size,start_pos + Vec3(i*side_length,0,0),box_visual)

    start_pos = Vec3(-num_boxes*side_length,0,8)
    for i in range(0,20):
      self.addBox("name %i"%(i),size,start_pos + Vec3(i*2*side_length,0,0),box_visual)


    # creating sphere
    diameter = 0.4
    sphere_visual = loader.loadModel('models/ball.egg')

    bounds = sphere_visual.getTightBounds() # start of model scaling
    extents = Vec3(bounds[1] - bounds[0])
    scale_factor = diameter/max([extents.getX(),extents.getY(),extents.getZ()])
    sphere_visual.clearModelNodes()
    sphere_visual.setScale(scale_factor,scale_factor,scale_factor) # end of model scaling

    sphere_visual.setTexture(loader.loadTexture('models/bowl.jpg'))
    sphere = self.world_node_.attachNewNode(BulletRigidBodyNode('Sphere'))
    sphere.node().addShape(BulletSphereShape(0.5*diameter))
    sphere.node().setMass(1.0)
    sphere.node().setLinearFactor((1,0,1))
    sphere.node().setAngularFactor((0,1,0))
    sphere.setCollideMask(BitMask32.allOn())
    sphere_visual.instanceTo(sphere)
    sphere.setPos(Vec3(0,0,size.getZ()+1))
    
    self.physics_world_.attachRigidBody(sphere.node())
    self.object_nodes_.append(sphere)
    self.controlled_objects_.append(sphere)

    # creating mobile box
    size = Vec3(0.2,0.2,0.4)
    mbox_visual = loader.loadModel('models/box.egg')
    bounds = mbox_visual.getTightBounds()
    extents = Vec3(bounds[1] - bounds[0])
    scale_factor = 1/max([extents.getX(),extents.getY(),extents.getZ()])
    mbox_visual.setScale(size.getX()*scale_factor,size.getY()*scale_factor,size.getZ()*scale_factor)

    mbox = self.world_node_.attachNewNode(BulletRigidBodyNode('MobileBox'))
    mbox.node().addShape(BulletBoxShape(size/2))
    mbox.node().setMass(1.0)
    mbox.node().setLinearFactor((1,0,1))
    mbox.node().setAngularFactor((0,1,0))
    mbox.setCollideMask(BitMask32.allOn())
    mbox_visual.instanceTo(mbox)
    mbox.setPos(Vec3(1,0,size.getZ()+1))
    
    self.physics_world_.attachRigidBody(mbox.node())
    self.object_nodes_.append(mbox)
    self.controlled_objects_.append(mbox)

    self.setupLevel()
    
    # Nodepath test
    np = NodePath('testnode0')    
    print('Created Nodepath to node %s'%(np.getName()))
    np.clear()    
    
    #np.attachNewNode(PandaNode('testnode1'))
    np.__init__(PandaNode('testnode1'))
    print('Attached new node %s to empty Nodepath'%(np.getName()))
示例#5
0
文件: dataIO.py 项目: tgbugs/desc
def treeMe(parent,
           positions,
           uuids,
           geomCollide,
           center=None,
           side=None,
           radius=None,
           request_hash=b'Fake',
           pipe=None):
    """ Divide the space covered by all the objects into an oct tree and then
        replace cubes with 512 objects with spheres radius = (side**2 / 2)**.5
        for some reason this massively improves performance even w/o the code
        for mouse over adding and removing subsets of nodes.
    """
    num_points = len(positions)

    if not num_points:
        return None

    if center == None:  # must use equality due to id changing across interpreters
        center = np.mean(positions, axis=0)
        norms = np.linalg.norm(positions - center, axis=1)
        radius = np.max(norms) * .5
        side = ((4 / 3) * radius**2)**.5
        if parent == None:
            l2Node = NodePath(CollisionNode('Root for %s 0' % request_hash))
        else:
            l2Node = parent.attachNewNode(
                CollisionNode('Root for %s 0' % request_hash))
    else:
        l2Node = parent.attachNewNode(
            CollisionNode(
                '%s.%s. %s' %
                (request_hash, center, int(parent.getName()[-2:]) + 1)))

    bitmasks = [np.zeros_like(uuids, dtype=np.bool_) for _ in range(8)
                ]  # ICK there must be a better way of creating bitmasks
    partition = positions > center

    #the 8 conbinatorial cases
    for i in range(num_points):
        index = octit(partition[i])
        bitmasks[index][i] = True

    next_leaves = []
    for i in range(8):
        branch = bitmasks[i]
        new_center = center + TREE_LOGIC[
            i] * side * .5  #FIXME we pay a price here when we calculate the center of an empty node
        subSet = positions[branch]
        if len(subSet):
            next_leaves.append(
                (l2Node, subSet, uuids[branch], geomCollide[branch],
                 new_center, side * .5, radius * .5, request_hash))

    #This method can also greatly accelerate the neighbor traversal because it reduces the total number of nodes needed
    if num_points < TREE_MAX_POINTS:
        leaf_max = np.max([len(tup[1]) for tup in next_leaves])
        if num_points < 4:
            c = np.mean(positions, axis=0)
            dists = []
            for p1 in positions:
                for p2 in positions:
                    if p1 is not p2:
                        d = np.linalg.norm(np.array(p2) - np.array(p1))
                        dists.append(d)
            r = np.max(dists) + np.mean(
                geomCollide) * 2  #max dists is the diameter so this is safe
            #l2Node = parent.attachNewNode(CollisionNode("%s.%s"%(request_hash,c)))
            l2Node.setName('leaf %s.%s. %s' %
                           (request_hash, c, int(parent.getName()[-2:]) + 1))
            l2Node.node().addSolid(CollisionSphere(c[0], c[1], c[2], r))
            l2Node.node().setIntoCollideMask(BitMask32.bit(BITMASK_COLL_MOUSE))
        elif leaf_max > num_points * .90:  # if any leaf has > half the points
            [treeMe(*leaf) for leaf in next_leaves]
            l2Node.setName('branch ' + l2Node.getName())
            l2Node.node().addSolid(
                CollisionSphere(center[0], center[1], center[2], radius * 2))
            l2Node.node().setIntoCollideMask(
                BitMask32.bit(BITMASK_COLL_MOUSE))  # this does not collide
            if pipe:  # extremely unlikely edge case
                print("hit an early pip")
                to_send = l2Node
                pipe.send(to_send)
                #for s in to_send:
                #pipe.send(s)
                pipe.close()
                return None
            else:
                return l2Node  # just for kicks even though all this is in place

        else:
            #l2Node = parent.attachNewNode(CollisionNode("%s.%s"%(request_hash,center)))
            l2Node.setName('leaf ' + l2Node.getName())
            l2Node.node().addSolid(
                CollisionSphere(center[0], center[1], center[2], radius * 2))
            l2Node.node().setIntoCollideMask(BitMask32.bit(BITMASK_COLL_MOUSE))

        for p, uuid, geom in zip(positions, uuids, geomCollide):
            childNode = l2Node.attachNewNode(CollisionNode("%s" %
                                                           uuid))  #XXX TODO
            childNode.node().addSolid(
                CollisionSphere(p[0], p[1], p[2], geom)
            )  # we do it this way because it keeps framerates WAY higher dont know why
            childNode.node().setIntoCollideMask(
                BitMask32.bit(BITMASK_COLL_CLICK))
            childNode.setTag('uuid', uuid)
        return l2Node
    else:  # we are a containing node
        #l2Node = parent.attachNewNode(CollisionNode("%s.%s.empty_parent"%(request_hash,center)))
        l2Node.setName('branch ' + l2Node.getName())
        l2Node.node().addSolid(
            CollisionSphere(center[0], center[1], center[2], radius * 2))
        l2Node.node().setIntoCollideMask(
            BitMask32.bit(BITMASK_COLL_MOUSE))  # this does not collide

    [treeMe(*leaf) for leaf in next_leaves]

    if pipe:
        to_send = l2Node
        pipe.send(to_send)
        #for s in to_send:
        #pipe.send(s)
        pipe.close()
    else:
        return l2Node  # just for kicks even though all this is in place
示例#6
0
文件: dataIO.py 项目: tgbugs/desc
def treeMe(parent, positions, uuids, geomCollide, center = None, side = None, radius = None, request_hash = b'Fake', pipe = None):
    """ Divide the space covered by all the objects into an oct tree and then
        replace cubes with 512 objects with spheres radius = (side**2 / 2)**.5
        for some reason this massively improves performance even w/o the code
        for mouse over adding and removing subsets of nodes.
    """
    num_points = len(positions)

    if not num_points:
        return None

    if center == None:  # must use equality due to id changing across interpreters
        center = np.mean(positions, axis=0)
        norms = np.linalg.norm(positions - center, axis = 1)
        radius = np.max(norms) * .5
        side = ((4/3) * radius**2) ** .5
        if parent == None:
            l2Node = NodePath(CollisionNode('Root for %s 0'%request_hash))
        else:
            l2Node = parent.attachNewNode(CollisionNode('Root for %s 0'%request_hash))
    else:
        l2Node = parent.attachNewNode(CollisionNode('%s.%s. %s'%(request_hash, center, int(parent.getName()[-2:]) + 1)))

    bitmasks =  [ np.zeros_like(uuids,dtype=np.bool_) for _ in range(8) ]  # ICK there must be a better way of creating bitmasks
    partition = positions > center
    
    #the 8 conbinatorial cases
    for i in range(num_points):
        index = octit(partition[i])
        bitmasks[index][i] = True

    next_leaves = []
    for i in range(8):
        branch = bitmasks[i]
        new_center = center + TREE_LOGIC[i] * side * .5  #FIXME we pay a price here when we calculate the center of an empty node
        subSet = positions[branch]
        if len(subSet):
            next_leaves.append((l2Node, subSet, uuids[branch], geomCollide[branch], new_center, side * .5, radius * .5, request_hash))

    #This method can also greatly accelerate the neighbor traversal because it reduces the total number of nodes needed
    if num_points < TREE_MAX_POINTS:
        leaf_max = np.max([len(tup[1]) for tup in next_leaves])
        if num_points < 4:
            c = np.mean(positions, axis=0)
            dists = []
            for p1 in positions:
                for p2 in positions:
                    if p1 is not p2:
                        d = np.linalg.norm(np.array(p2) - np.array(p1))
                        dists.append(d)
            r = np.max(dists) + np.mean(geomCollide) * 2  #max dists is the diameter so this is safe
            #l2Node = parent.attachNewNode(CollisionNode("%s.%s"%(request_hash,c)))
            l2Node.setName('leaf %s.%s. %s'%(request_hash, c, int(parent.getName()[-2:]) + 1))
            l2Node.node().addSolid(CollisionSphere(c[0],c[1],c[2],r))
            l2Node.node().setIntoCollideMask(BitMask32.bit(BITMASK_COLL_MOUSE))
        elif leaf_max > num_points * .90:  # if any leaf has > half the points
            [treeMe(*leaf) for leaf in next_leaves]
            l2Node.setName('branch '+l2Node.getName())
            l2Node.node().addSolid(CollisionSphere(center[0],center[1],center[2],radius * 2))
            l2Node.node().setIntoCollideMask(BitMask32.bit(BITMASK_COLL_MOUSE))  # this does not collide
            if pipe:  # extremely unlikely edge case
                print("hit an early pip")
                to_send = l2Node
                pipe.send(to_send)
                #for s in to_send:
                    #pipe.send(s)
                pipe.close()
                return None
            else:
                return l2Node  # just for kicks even though all this is in place

        else:
            #l2Node = parent.attachNewNode(CollisionNode("%s.%s"%(request_hash,center)))
            l2Node.setName('leaf '+l2Node.getName())
            l2Node.node().addSolid(CollisionSphere(center[0],center[1],center[2],radius * 2))
            l2Node.node().setIntoCollideMask(BitMask32.bit(BITMASK_COLL_MOUSE))

        for p,uuid,geom in zip(positions,uuids,geomCollide):
            childNode = l2Node.attachNewNode(CollisionNode("%s"%uuid))  #XXX TODO
            childNode.node().addSolid(CollisionSphere(p[0],p[1],p[2],geom)) # we do it this way because it keeps framerates WAY higher dont know why
            childNode.node().setIntoCollideMask(BitMask32.bit(BITMASK_COLL_CLICK))
            childNode.setTag('uuid',uuid)
        return l2Node
    else:  # we are a containing node
        #l2Node = parent.attachNewNode(CollisionNode("%s.%s.empty_parent"%(request_hash,center)))
        l2Node.setName('branch '+l2Node.getName())
        l2Node.node().addSolid(CollisionSphere(center[0],center[1],center[2],radius * 2))
        l2Node.node().setIntoCollideMask(BitMask32.bit(BITMASK_COLL_MOUSE))  # this does not collide

    [treeMe(*leaf) for leaf in next_leaves]

    if pipe:
        to_send = l2Node
        pipe.send(to_send)
        #for s in to_send:
            #pipe.send(s)
        pipe.close()
    else:
        return l2Node  # just for kicks even though all this is in place
示例#7
0
class PopupMenu(DirectObject):
    '''
  A class to create a popup or context menu.
  Features :
    [1] it's destroyed by pressing ESCAPE, or LMB/RMB click outside of it
    [2] menu item's command is executed by pressing ENTER/RETURN or SPACE when
        it's hilighted
    [3] you can use arrow UP/DOWN to navigate
    [4] separator lines
    [5] menu item image
    [6] menu item hotkey
        If there are more than 1 item using the same hotkey,
        those items will be hilighted in cycle when the hotkey is pressed.
    [7] shortcut key text at the right side of menu item
    [8] multiple lines item text
    [9] menu item can have sub menus
    [10] it's offscreen-proof, try to put your pointer next to screen edge or corner
        before creating it
  '''
    grayImages = {}  # storage of grayed images,

    # so the same image will be converted to grayscale only once

    def __init__(self,
                 items,
                 parent=None,
                 buttonThrower=None,
                 onDestroy=None,
                 font=None,
                 baselineOffset=.0,
                 scale=.05,
                 itemHeight=1.,
                 leftPad=.0,
                 separatorHeight=.5,
                 underscoreThickness=1,
                 BGColor=(0, 0, 0, .7),
                 BGBorderColor=(1, .85, .4, 1),
                 separatorColor=(1, 1, 1, 1),
                 frameColorHover=(1, .85, .4, 1),
                 frameColorPress=(0, 1, 0, 1),
                 textColorReady=(1, 1, 1, 1),
                 textColorHover=(0, 0, 0, 1),
                 textColorPress=(0, 0, 0, 1),
                 textColorDisabled=(.5, .5, .5, 1),
                 minZ=None,
                 useMouseZ=True):
        '''
      items : a collection of menu items
         Item format :
            ( 'Item text', 'path/to/image', command )
                        OR
            ( 'Item text', 'path/to/image', command, arg1,arg2,.... )
         If you don't want to use an image, pass 0.

         To create disabled item, pass 0 for the command :
            ( 'Item text', 'path/to/image', 0 )
         so, you can easily switch between enabled or disabled :
            ( 'Item text', 'path/to/image', command if commandEnabled else 0 )
                        OR
            ( 'Item text', 'path/to/image', (0,command)[commandEnabled] )

         To create submenu, pass a sequence of submenu items for the command.
         To create disabled submenu, pass an empty sequence for the command.

         To enable hotkey, insert an underscore before the character,
         e.g. hotkey of 'Item te_xt' is 'x' key.

         To add shortcut key text at the right side of the item, append it at the end of
         the item text, separated by "more than" sign, e.g. 'Item text>Ctrl-T'.

         To insert separator line, pass 0 for the whole item.


      parent : where to attach the menu, defaults to aspect2d

      buttonThrower : button thrower whose thrown events are blocked temporarily
                      when the menu is displayed. If not given, the default
                      button thrower is used

      onDestroy : user function which will be called after the menu is fully destroyed
      
      font           : text font
      baselineOffset : text's baseline Z offset

      scale       : text scale
      itemHeight  : spacing between items, defaults to 1
      leftPad     : blank space width before text
      separatorHeight : separator line height, relative to itemHeight

      underscoreThickness : underscore line thickness

      BGColor, BGBorderColor, separatorColor, frameColorHover, frameColorPress,
      textColorReady, textColorHover, textColorPress, textColorDisabled
      are some of the menu components' color

      minZ : minimum Z position to restrain menu's bottom from going offscreen (-1..1).
             If it's None, it will be set a little above the screen's bottom.
      '''
        self.parent = parent if parent else aspect2d
        self.onDestroy = onDestroy
        self.BT = buttonThrower if buttonThrower else base.buttonThrowers[
            0].node()
        self.menu = NodePath('menu-%s' % id(self))
        self.parentMenu = None
        self.submenu = None
        self.BTprefix = self.menu.getName() + '>'
        self.submenuCreationTaskName = 'createSubMenu-' + self.BTprefix
        self.submenuRemovalTaskName = 'removeSubMenu-' + self.BTprefix
        self.font = font if font else TextNode.getDefaultFont()
        self.baselineOffset = baselineOffset
        if isinstance(scale, (float, int)):
            scale = (scale, 1.0, scale)
        self.scale = scale
        self.itemHeight = itemHeight
        self.leftPad = leftPad
        self.separatorHeight = separatorHeight
        self.underscoreThickness = underscoreThickness
        self.BGColor = BGColor
        self.BGBorderColor = BGBorderColor
        self.separatorColor = separatorColor
        self.frameColorHover = frameColorHover
        self.frameColorPress = frameColorPress
        self.textColorReady = textColorReady
        self.textColorHover = textColorHover
        self.textColorPress = textColorPress
        self.textColorDisabled = textColorDisabled
        self.minZ = minZ
        self.mpos = Point2(base.mouseWatcherNode.getMouse())

        self.itemCommand = []
        self.hotkeys = {}
        self.numItems = 0
        self.sel = -1
        self.selByKey = False

        bgPad = self.bgPad = .0125
        texMargin = self.font.getTextureMargin() * self.scale[0] * .25
        b = DirectButton(parent=NodePath(''),
                         text='^|g_',
                         text_font=self.font,
                         scale=self.scale)
        fr = b.node().getFrame()
        b.getParent().removeNode()
        baselineToCenter = (fr[2] + fr[3]) * self.scale[0]
        LH = (fr[3] - fr[2]) * self.itemHeight * self.scale[2]
        imageHalfHeight = .5 * (fr[3] - fr[2]) * self.itemHeight * .85
        arrowHalfHeight = .5 * (fr[3] - fr[2]) * self.itemHeight * .5
        baselineToTop = (fr[3] * self.itemHeight * self.scale[2] /
                         LH) / (1. + self.baselineOffset)
        baselineToBot = LH / self.scale[2] - baselineToTop
        itemZcenter = (baselineToTop - baselineToBot) * .5
        separatorHalfHeight = .5 * separatorHeight * LH
        LSseparator = LineSegs()
        LSseparator.setColor(.5, .5, .5, .2)

        arrowVtx = [
            (0, itemZcenter),
            (-2 * arrowHalfHeight, itemZcenter + arrowHalfHeight),
            (-arrowHalfHeight, itemZcenter),
            (-2 * arrowHalfHeight, itemZcenter - arrowHalfHeight),
        ]
        tri = Triangulator()
        vdata = GeomVertexData('trig', GeomVertexFormat.getV3(), Geom.UHStatic)
        vwriter = GeomVertexWriter(vdata, 'vertex')
        for x, z in arrowVtx:
            vi = tri.addVertex(x, z)
            vwriter.addData3f(x, 0, z)
            tri.addPolygonVertex(vi)
        tri.triangulate()
        prim = GeomTriangles(Geom.UHStatic)
        for i in range(tri.getNumTriangles()):
            prim.addVertices(tri.getTriangleV0(i), tri.getTriangleV1(i),
                             tri.getTriangleV2(i))
            prim.closePrimitive()
        geom = Geom(vdata)
        geom.addPrimitive(prim)
        geomNode = GeomNode('arrow')
        geomNode.addGeom(geom)
        realArrow = NodePath(geomNode)
        z = -baselineToTop * self.scale[2] - bgPad
        maxWidth = .1 / self.scale[0]
        shortcutTextMaxWidth = 0
        anyImage = False
        anyArrow = False
        anyShortcut = False
        arrows = []
        shortcutTexts = []
        loadPrcFileData('', 'text-flatten 0')
        for item in items:
            if item:
                t, imgPath, f = item[:3]
                haveSubmenu = type(f) in SEQUENCE_TYPES
                anyArrow |= haveSubmenu
                anyImage |= isinstance(imgPath, bool) or bool(imgPath)
                disabled = not len(f) if haveSubmenu else not callable(f)
                args = item[3:]
                underlinePos = t.find('_')
                t = t.replace('_', '')
                shortcutSepPos = t.find('>')
                if shortcutSepPos > -1:
                    if haveSubmenu:
                        print(
                            "\nA SHORTCUT KEY POINTING TO A SUBMENU IS NON-SENSE, DON'T YOU AGREE ?"
                        )
                    else:
                        shortcutText = NodePath(
                            OnscreenText(
                                parent=self.menu,
                                text=t[shortcutSepPos + 1:],
                                font=self.font,
                                scale=1,
                                fg=(1, 1, 1, 1),
                                align=TextNode.ARight,
                            ))
                        shortcutTextMaxWidth = max(
                            shortcutTextMaxWidth,
                            abs(shortcutText.getTightBounds()[0][0]))
                        anyShortcut = True
                    t = t[:shortcutSepPos]
                else:
                    shortcutText = ''
                EoLcount = t.count('\n')
                arrowZpos = -self.font.getLineHeight() * EoLcount * .5
                if disabled:
                    b = NodePath(
                        OnscreenText(
                            parent=self.menu,
                            text=t,
                            font=self.font,
                            scale=1,
                            fg=textColorDisabled,
                            align=TextNode.ALeft,
                        ))
                    # don't pass the scale and position to OnscreenText constructor,
                    # to maintain correctness between the OnscreenText and DirectButton items
                    # due to the new text generation implementation
                    b.setScale(self.scale)
                    b.setZ(z)
                    maxWidth = max(maxWidth,
                                   b.getTightBounds()[1][0] / self.scale[0])
                    if shortcutText:
                        shortcutText.reparentTo(b)
                        shortcutText.setColor(Vec4(*textColorDisabled), 1)
                        shortcutText.setZ(arrowZpos)
                        shortcutTexts.append(shortcutText)
                else:
                    b = DirectButton(
                        parent=self.menu,
                        text=t,
                        text_font=self.font,
                        scale=self.scale,
                        pos=(0, 0, z),
                        text_fg=textColorReady,
                        # text color when mouse over
                        text2_fg=textColorHover,
                        # text color when pressed
                        text1_fg=textColorHover
                        if haveSubmenu else textColorPress,
                        # framecolor when pressed
                        frameColor=frameColorHover
                        if haveSubmenu else frameColorPress,
                        commandButtons=[DGG.LMB, DGG.RMB],
                        command=(lambda: 0)
                        if haveSubmenu else self.__runCommand,
                        extraArgs=[] if haveSubmenu else [f, args],
                        text_align=TextNode.ALeft,
                        relief=DGG.FLAT,
                        rolloverSound=0,
                        clickSound=0,
                        pressEffect=0)
                    b.stateNodePath[2].setColor(
                        *frameColorHover)  # framecolor when mouse over
                    b.stateNodePath[0].setColor(0, 0, 0,
                                                0)  # framecolor when ready
                    bframe = Vec4(b.node().getFrame())
                    if EoLcount:
                        bframe.setZ(EoLcount * 10)
                        b['frameSize'] = bframe
                    maxWidth = max(maxWidth, bframe[1])
                    if shortcutText:
                        for snpi, col in ((0, textColorReady),
                                          (1, textColorPress),
                                          (2, textColorHover)):
                            sct = shortcutText.copyTo(b.stateNodePath[snpi],
                                                      sort=10)
                            sct.setColor(Vec4(*col), 1)
                            sct.setZ(arrowZpos)
                            shortcutTexts.append(sct)
                        shortcutText.removeNode()
                if isinstance(imgPath, bool):
                    if imgPath:
                        if disabled:
                            fg = textColorDisabled
                        else:
                            fg = textColorReady
                        tick = NodePath(
                            OnscreenText(
                                parent=b,
                                text=u"\u2714",
                                font=self.font,
                                scale=1,
                                fg=fg,
                                align=TextNode.ALeft,
                            ))
                        tick.setX(-2 * imageHalfHeight - leftPad)
                elif imgPath:
                    img = loader.loadTexture(imgPath, okMissing=True)
                    if img is not None:
                        if disabled:
                            if imgPath in PopupMenu.grayImages:
                                img = PopupMenu.grayImages[imgPath]
                            else:
                                pnm = PNMImage()
                                img.store(pnm)
                                pnm.makeGrayscale(.2, .2, .2)
                                img = Texture()
                                img.load(pnm)
                                PopupMenu.grayImages[imgPath] = img
                        img.setMinfilter(Texture.FTLinearMipmapLinear)
                        img.setWrapU(Texture.WMClamp)
                        img.setWrapV(Texture.WMClamp)
                        CM = CardMaker('')
                        CM.setFrame(-2 * imageHalfHeight - leftPad, -leftPad,
                                    itemZcenter - imageHalfHeight,
                                    itemZcenter + imageHalfHeight)
                        imgCard = b.attachNewNode(CM.generate())
                        imgCard.setTexture(img)
                if underlinePos > -1:
                    oneLineText = t[:underlinePos + 1]
                    oneLineText = oneLineText[oneLineText.rfind('\n') + 1:]
                    tn = TextNode('')
                    tn.setFont(self.font)
                    tn.setText(oneLineText)
                    tnp = NodePath(tn.getInternalGeom())
                    underlineXend = tnp.getTightBounds()[1][0]
                    tnp.removeNode()
                    tn.setText(t[underlinePos])
                    tnp = NodePath(tn.getInternalGeom())
                    b3 = tnp.getTightBounds()
                    underlineXstart = underlineXend - (b3[1] - b3[0])[0]
                    tnp.removeNode()
                    underlineZpos = -.7 * baselineToBot - self.font.getLineHeight(
                    ) * t[:underlinePos].count('\n')
                    LSunder = LineSegs()
                    LSunder.setThickness(underscoreThickness)
                    LSunder.moveTo(underlineXstart + texMargin, 0,
                                   underlineZpos)
                    LSunder.drawTo(underlineXend - texMargin, 0, underlineZpos)
                    if disabled:
                        underline = b.attachNewNode(LSunder.create())
                        underline.setColor(Vec4(*textColorDisabled), 1)
                    else:
                        underline = b.stateNodePath[0].attachNewNode(
                            LSunder.create())
                        underline.setColor(Vec4(*textColorReady), 1)
                        underline.copyTo(b.stateNodePath[1], 10).setColor(
                            Vec4(*textColorHover
                                 if haveSubmenu else textColorPress), 1)
                        underline.copyTo(b.stateNodePath[2],
                                         10).setColor(Vec4(*textColorHover), 1)
                        hotkey = t[underlinePos].lower()
                        if hotkey in self.hotkeys:
                            self.hotkeys[hotkey].append(self.numItems)
                        else:
                            self.hotkeys[hotkey] = [self.numItems]
                            self.accept(self.BTprefix + hotkey,
                                        self.__processHotkey, [hotkey])
                            self.accept(self.BTprefix + 'alt-' + hotkey,
                                        self.__processHotkey, [hotkey])
                if haveSubmenu:
                    if disabled:
                        arrow = realArrow.instanceUnderNode(b, '')
                        arrow.setColor(Vec4(*textColorDisabled), 1)
                        arrow.setZ(arrowZpos)
                    else:
                        arrow = realArrow.instanceUnderNode(
                            b.stateNodePath[0], 'r')
                        arrow.setColor(Vec4(*textColorReady), 1)
                        arrow.setZ(arrowZpos)
                        arrPress = realArrow.instanceUnderNode(
                            b.stateNodePath[1], 'p')
                        arrPress.setColor(Vec4(*textColorHover), 1)
                        arrPress.setZ(arrowZpos)
                        arrHover = realArrow.instanceUnderNode(
                            b.stateNodePath[2], 'h')
                        arrHover.setColor(Vec4(*textColorHover), 1)
                        arrHover.setZ(arrowZpos)
                        # weird, if sort order is 0, it's obscured by the frame
                        for a in (arrPress, arrHover):
                            a.reparentTo(a.getParent(), sort=10)
                if not disabled:
                    extraArgs = [self.numItems, f if haveSubmenu else 0]
                    self.accept(DGG.ENTER + b.guiId, self.__hoverOnItem,
                                extraArgs)
                    self.accept(DGG.EXIT + b.guiId, self.__offItem)
                    #~ self.itemCommand.append((None,0) if haveSubmenu else (f,args))
                    self.itemCommand.append((f, args))
                    if self.numItems == 0:
                        self.firstButtonIdx = int(b.guiId[2:])
                    self.numItems += 1
                z -= LH + self.font.getLineHeight() * self.scale[2] * EoLcount
            else:  # SEPARATOR LINE
                z += LH - separatorHalfHeight - baselineToBot * self.scale[2]
                LSseparator.moveTo(0, 0, z)
                LSseparator.drawTo(self.scale[0] * .5, 0, z)
                LSseparator.drawTo(self.scale[0], 0, z)
                z -= separatorHalfHeight + baselineToTop * self.scale[2]
        maxWidth += 7 * arrowHalfHeight * (
            anyArrow or anyShortcut) + .2 + shortcutTextMaxWidth
        arrowXpos = maxWidth - arrowHalfHeight
        realArrow.setX(arrowXpos)
        if anyImage:
            leftPad += 2 * imageHalfHeight + leftPad
        for sct in shortcutTexts:
            sct.setX(maxWidth - 2 * (arrowHalfHeight * anyArrow + .2))
        for c in asList(self.menu.findAllMatches('**/DirectButton*')):
            numLines = c.node().getFrame()[2]
            c.node().setFrame(
                Vec4(
                    -leftPad, maxWidth, -baselineToBot -
                    (numLines * .1 * self.itemHeight if numLines >= 10 else 0),
                    baselineToTop))
        loadPrcFileData('', 'text-flatten 1')

        try:
            minZ = self.menu.getChild(0).getRelativePoint(
                b, Point3(0, 0,
                          b.node().getFrame()[2]))[2]
        except:
            minZ = self.menu.getChild(0).getRelativePoint(
                self.menu, Point3(
                    0, 0,
                    b.getTightBounds()[0][2]))[2] - baselineToBot * .5
        try:
            top = self.menu.getChild(0).node().getFrame()[3]
        except:
            top = self.menu.getChild(0).getZ() + baselineToTop
        l, r, b, t = -leftPad - bgPad / self.scale[
            0], maxWidth + bgPad / self.scale[0], minZ - bgPad / self.scale[
                2], top + bgPad / self.scale[2]
        menuBG = DirectFrame(parent=self.menu.getChild(0),
                             frameSize=(l, r, b, t),
                             frameColor=BGColor,
                             state=DGG.NORMAL,
                             suppressMouse=1)
        menuBorder = self.menu.getChild(0).attachNewNode('border')
        borderVtx = (
            (l, 0, b),
            (l, 0, .5 * (b + t)),
            (l, 0, t),
            (.5 * (l + r), 0, t),
            (r, 0, t),
            (r, 0, .5 * (b + t)),
            (r, 0, b),
            (.5 * (l + r), 0, b),
            (l, 0, b),
        )
        LSborderBG = LineSegs()
        LSborderBG.setThickness(4)
        LSborderBG.setColor(0, 0, 0, .7)
        LSborderBG.moveTo(*(borderVtx[0]))
        for v in borderVtx[1:]:
            LSborderBG.drawTo(*v)
        # fills the gap at corners
        for v in range(0, 7, 2):
            LSborderBG.moveTo(*(borderVtx[v]))
        menuBorder.attachNewNode(LSborderBG.create())
        LSborder = LineSegs()
        LSborder.setThickness(2)
        LSborder.setColor(*BGBorderColor)
        LSborder.moveTo(*(borderVtx[0]))
        for v in borderVtx[1:]:
            LSborder.drawTo(*v)
        menuBorder.attachNewNode(LSborder.create())
        for v in range(1, 8, 2):
            LSborderBG.setVertexColor(v, Vec4(0, 0, 0, .1))
            LSborder.setVertexColor(v, Vec4(.3, .3, .3, .5))
        menuBorderB3 = menuBorder.getTightBounds()
        menuBorderDims = menuBorderB3[1] - menuBorderB3[0]
        menuBG.wrtReparentTo(self.menu, sort=-1)
        self.menu.reparentTo(self.parent)
        x = -menuBorderB3[0][0] * self.scale[0]
        for c in asList(self.menu.getChildren()):
            c.setX(x)
        self.maxWidth = maxWidth = menuBorderDims[0]
        self.height = menuBorderDims[2]
        maxWidthR2D = maxWidth * self.menu.getChild(0).getSx(render2d)
        separatorLines = self.menu.attachNewNode(LSseparator.create(), 10)
        separatorLines.setSx(maxWidth)
        for v in range(1, LSseparator.getNumVertices(), 3):
            LSseparator.setVertexColor(v, Vec4(*separatorColor))
        x = clamp(-.98, .98 - maxWidthR2D, self.mpos[0] - maxWidthR2D * .5)
        minZ = (-.98 if self.minZ is None else self.minZ)
        z = clamp(
            minZ +
            menuBorderDims[2] * self.scale[2] * self.parent.getSz(render2d),
            .98, self.mpos[1] if useMouseZ else -1000)
        self.menu.setPos(render2d, x, 0, z)
        self.menu.setTransparency(1)

        self.origBTprefix = self.BT.getPrefix()
        self.BT.setPrefix(self.BTprefix)
        self.accept(self.BTprefix + 'escape', self.destroy)
        for e in ('mouse1', 'mouse3'):
            self.accept(self.BTprefix + e, self.destroy, [True])
        self.accept(self.BTprefix + 'arrow_down', self.__nextItem)
        self.accept(self.BTprefix + 'arrow_down-repeat', self.__nextItem)
        self.accept(self.BTprefix + 'arrow_up', self.__prevItem)
        self.accept(self.BTprefix + 'arrow_up-repeat', self.__prevItem)
        self.accept(self.BTprefix + 'enter', self.__runSelItemCommand)
        self.accept(self.BTprefix + 'space', self.__runSelItemCommand)

    def __offItem(self, crap):
        self.sel = -1
        self.__cancelSubmenuCreation()

    def __hoverOnItem(self, idx, menu, crap):
        self.sel = idx
        self.__cancelSubmenuCreation()
        if self.BT.getPrefix()==self.BTprefix or \
             (self.submenu and self.submenuIdx==idx):
            self.__cancelSubmenuRemoval()
        if menu:
            if not (self.submenu and self.submenuIdx == idx):
                #~ if self.selByKey:
                #~ self.selByKey=False
                #~ self.__createSubmenu(idx,menu)
                #~ else:
                taskMgr.doMethodLater(.3,
                                      self.__createSubmenu,
                                      self.submenuCreationTaskName,
                                      extraArgs=[idx, menu])
        else:
            taskMgr.doMethodLater(.5,
                                  self.__removeSubmenu,
                                  self.submenuRemovalTaskName,
                                  extraArgs=[])

    def __cancelSubmenuCreation(self):
        taskMgr.removeTasksMatching('createSubMenu-*')

    def __createSubmenu(self, idx, menu):
        self.__cancelSubmenuCreation()
        self.__removeSubmenu()
        self.submenu = PopupMenu(items=menu,
                                 parent=self.parent,
                                 buttonThrower=self.BT,
                                 font=self.font,
                                 baselineOffset=self.baselineOffset,
                                 scale=self.scale,
                                 itemHeight=self.itemHeight,
                                 leftPad=self.leftPad,
                                 separatorHeight=self.separatorHeight,
                                 underscoreThickness=self.underscoreThickness,
                                 BGColor=self.BGColor,
                                 BGBorderColor=self.BGBorderColor,
                                 separatorColor=self.separatorColor,
                                 frameColorHover=self.frameColorHover,
                                 frameColorPress=self.frameColorPress,
                                 textColorReady=self.textColorReady,
                                 textColorHover=self.textColorHover,
                                 textColorPress=self.textColorPress,
                                 textColorDisabled=self.textColorDisabled,
                                 minZ=self.minZ,
                                 useMouseZ=False)
        self.submenuIdx = idx
        self.submenu.parentMenu = self
        if self.menu.getBinName():
            self.submenu.menu.setBin(self.menu.getBinName(),
                                     self.menu.getBinDrawOrder() + 1)
        sb3 = self.submenu.menu.getTightBounds()
        sb = sb3[1] - sb3[0]
        b3 = self.menu.getTightBounds()
        x = b3[1][0]
        if render2d.getRelativePoint(self.parent, Point3(x + sb[0], 0,
                                                         0))[0] > .98:
            x = b3[0][0] - sb[0]
        if render2d.getRelativePoint(self.parent, Point3(x, 0, 0))[0] < -.98:
            x = self.parent.getRelativePoint(render2d, Point3(-.98, 0, 0))[0]
        item = self.menu.find('**/*-pg%s' % (self.firstButtonIdx + idx))
        z = self.parent.getRelativePoint(
            item, Point3(0, 0,
                         item.node().getFrame()[3]))[2] + self.bgPad
        self.submenu.menu.setPos(x, 0, max(z, self.submenu.menu.getZ()))


#       self.submenu.menu.setPos(x,0,z)

    def __nextItem(self):
        if self.numItems:
            self.sel = clamp(0, self.numItems - 1, self.sel + 1)
            self.__putPointerAtItem()
            self.selByKey = True

    def __prevItem(self):
        if self.numItems:
            self.sel = clamp(0, self.numItems - 1,
                             (self.sel -
                              1) if self.sel > -1 else self.numItems - 1)
            self.__putPointerAtItem()
            self.selByKey = True

    def __putPointerAtItem(self):
        item = self.menu.find('**/*-pg%s' % (self.firstButtonIdx + self.sel))
        fr = item.node().getFrame()
        c = Point3(.5 * (fr[0] + fr[1]), 0, .5 * (fr[2] + fr[3]))
        cR2D = render2d.getRelativePoint(item, c)
        x, y = int(base.win.getXSize() * .5 * (cR2D[0] + 1)), int(
            base.win.getYSize() * .5 * (-cR2D[2] + 1))
        if '__origmovePointer' in base.win.DtoolClassDict:
            base.win.DtoolClassDict['__origmovePointer'](base.win, 0, x, y)
        else:
            base.win.movePointer(0, x, y)

    def __processHotkey(self, hotkey):
        itemsIdx = self.hotkeys[hotkey]
        if len(itemsIdx) == 1 and type(
                self.itemCommand[itemsIdx[0]][0]) not in SEQUENCE_TYPES:
            self.__runCommand(*self.itemCommand[itemsIdx[0]])
        else:
            if self.sel in itemsIdx:
                idx = itemsIdx.index(self.sel) + 1
                idx %= len(itemsIdx)
                self.sel = itemsIdx[idx]
            else:
                self.sel = itemsIdx[0]
            self.selByKey = True
            # if it's already there, putting the pointer doesn't trigger the 'enter'
            # event, so just bypass it
            if not (self.submenu and self.submenuIdx==self.sel) and\
                 type(self.itemCommand[itemsIdx[0]][0]) in SEQUENCE_TYPES:
                self.__createSubmenu(self.sel,
                                     self.itemCommand[itemsIdx[0]][0])
            self.__putPointerAtItem()

    def __doRunCommand(self, f, args):
        self.destroy(delParents=True)
        f(*args)

    def __runCommand(self, f, args):
        if callable(f):
            # must be done at next frame, so shortcut key event won't bleed to the scene
            taskMgr.doMethodLater(.01,
                                  self.__doRunCommand,
                                  'run menu command',
                                  extraArgs=[f, args])

    def __runSelItemCommand(self):
        if self.sel == -1: return
        self.__runCommand(*self.itemCommand[self.sel])

    def __cancelSubmenuRemoval(self):
        taskMgr.removeTasksMatching('removeSubMenu-*')

    def __removeSubmenu(self):
        self.__cancelSubmenuRemoval()
        if self.submenu:
            self.submenu.destroy()

    def destroy(self, delParents=False):
        self.__cancelSubmenuCreation()
        self.__removeSubmenu()
        self.subMenu = None
        self.ignoreAll()
        self.menu.removeNode()
        #       if self.origBTprefix.find('menu-')==-1:
        #          taskMgr.step()
        self.BT.setPrefix(self.origBTprefix)
        messenger.send(self.BTprefix + 'destroyed')
        if delParents and self.parentMenu:
            parent = self.parentMenu
            while parent.parentMenu:
                parent = parent.parentMenu
            parent.destroy()
        if self.parentMenu:
            self.parentMenu.submenuIdx = None
            self.parentMenu = None
        if callable(self.onDestroy):
            self.onDestroy()