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
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()))
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
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
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()