class Grabber(object): def __init__( self, levitNP): """ A widget to position, rotate, and scale Panda 3D Models and Actors * handleM1 decides what to do with a mouse1 click -- object selection by calling handleSelection when the grabModel is inactive (hidden) -- object manipulation by calling handleManipulationSetup (sets the stage for and launches the dragTask) isHidden() when nothing is selected isDragging means not running collision checks for selection setup and LMB is pressed call handleM1 from another class to push control up in the program hierarchy (remove inner class calls) """ # TODO remove selection functionality from grabber and put it in a selector class self.levitorNP = levitNP # TODO remove this and use barebonesNP self.selected = None self.initialize() def initialize(self): """Reset everything except LevitorNP and selected, also called inside __init__""" self.notify = DirectNotify().newCategory('grabberErr') self.currPlaneColNorm = Vec3(0.0) self.isCameraControlOn = False self.isDragging = False self.isMultiselect = False self.grabScaleFactor = .075 self.currTransformDir = Point3(0.0) self.interFrameMousePosition = Point3(0.0) self.init3DemVal = Point3(0.0) # initCommVal holds the value before a command operation has taken place self.initialCommandTrgVal = None # To load the grabber model, this climbs up the absolute path to /barebones/ to gain access to the model folder self.grabModelNP = loader.loadModel(Filename.fromOsSpecific( ntpath.split( ntpath.split(inspect.stack()[0][1])[0] )[0]) + '/EditorModels/widget') self.grabModelNP.setPos(0.0, 0.0, 0.0) self.grabModelNP.setBin("fixed", 40) self.grabModelNP.setDepthTest(False) self.grabModelNP.setDepthWrite(False) self.transformOpEnum = Enum('rot, scale, trans') self.currTransformOperation = None # TODO For readability, use this enum in the nested if/else as was the original intent. self.grabInd = Enum('xRot, yRot, zRot, xScaler, yScaler, zScaler, xTrans, yTrans, zTrans, xyTrans, xzTrans, zyTrans, grabCore') grbrNodLst = [self.grabModelNP.find("**/XRotator;+h-s-i"), # 0 self.grabModelNP.find("**/YRotator;+h-s-i"), # 1 self.grabModelNP.find("**/ZRotator;+h-s-i"), # 2 end rotate self.grabModelNP.find("**/XScaler;+h-s-i"), # 3 self.grabModelNP.find("**/YScaler;+h-s-i"), # 4 self.grabModelNP.find("**/ZScaler;+h-s-i"), # 5 end scale self.grabModelNP.find("**/XTranslator;+h-s-i"), # 6 self.grabModelNP.find("**/YTranslator;+h-s-i"), # 7 self.grabModelNP.find("**/ZTranslator;+h-s-i"), # 8 end translate / end single dir operations self.grabModelNP.find("**/XYTranslator;+h-s-i"), # 9 self.grabModelNP.find("**/XZTranslator;+h-s-i"), # 10 self.grabModelNP.find("**/ZYTranslator;+h-s-i"), # 11 end bi-directional operations self.grabModelNP.find("**/WidgetCore;+h-s-i")] # 12 #Mat4.yToZUpMat() # change coordinate to z up grbrNodLst[12].getParent().setHprScale(0, 0, 0, 1, 1, -1) self.grabModelNP.setPythonTag('grabberRoot', grbrNodLst) self.grabModelNP.reparentTo(BBGlobalVars.bareBonesObj.levitorNP) self.grabModelNP.hide() #self.grabIntoBitMask = COLLISIONMASKS self.grabModelNP.setCollideMask(COLLISIONMASKS['default']) self.grabModelNP.setPythonTag('grabber', self) ############################################################################## # This whole section is the basics for setting up mouse selection # --The mouse events are added in the events section (next) # Create the collision node for the picker ray to add traverser as a 'from' collider self.grabberColNode = CollisionNode('grabberMouseRay') # Set the collision bitmask # TODO: define collision bitmask (let user define thiers? likely not) self.defaultBitMask = GeomNode.getDefaultCollideMask() self.grabberColNode.setFromCollideMask(self.defaultBitMask) self.grabberRayColNP = camera.attachNewNode(self.grabberColNode) # Create the grabberRay and add it to the picker CollisionNode self.grabberRay = CollisionRay(0.0, 0.0, 0.0, 0.0, 1.0, 0.0) self.grabberRayNP = self.grabberColNode.addSolid(self.grabberRay) # create a collision queue for the traverser self.colHandlerQueue = CollisionHandlerQueue() # Create collision traverser self.colTraverser = CollisionTraverser('grabberTraverser') # Set the collision traverser's 'fromObj' and handler # e.g. trav.addCollider( fromObj, handler ) self.colTraverser.addCollider(self.grabberRayColNP, self.colHandlerQueue) ############################################################ # setup event handling with the messenger # URGENT remove all of this messenger code throughout Grabber, especially the camera control # disable the mouse when the ~ is pressed (w/o shift) self.disableCamera() # disable camera control by the mouse messenger.accept('`', self, self.enableCamera) # enable camera control when the ~ key is pressed w/o shift messenger.accept('`-up', self, self.disableCamera) # disable camera control when the ~ key is released # handle mouse selection/deselection & manipulating the scene messenger.accept('mouse1', self, self.handleM1, persistent=1) # deselect in event handler taskMgr.add(self.scaleGrabber, 'scaleGrabber') # //////////////////////////////////////////////////////////////////// # comment out: good for debug info #taskMgr.add(self.watchMouseColl, name='grabberDebug') #this is only good for seeing types and hierarchy #self.grabModelNP.ls() #render.ls() # self.frames = 0 #remove # self.axis = loader.loadModel("zup-axis") # self.axis.reparentTo(self.grabModelNP) # self.axis.setScale(.15) # self.axis.setPos(0.0) # self.grabModelNP.append( 'newAttrib', self) # setattr( self.grabModelNP, 'newAttrib', self) def prepareForPickle(self): self.colTraverser = None # Traversers are not picklable self.defaultBitMask = None # BitMasks "..." # self.grabIntoBitMask = None # "..." self.colHandlerQueue = None # CollisonHandlerQueue "..." self.grabModelNP.removeNode() self.grabModelNP = None taskMgr.remove('scaleGrabber') def recoverFromPickle(self): self.initialize() if self.selected is not None: self.grabModelNP.setPos(render, self.selected.getPos(render)) self.grabModelNP.show() print "grabber sel ", self.selected, " isHidden() ", self.grabModelNP.isHidden(), '\n' taskMgr.add(self.scaleGrabber, 'scaleGrabber') #### May use to gain control over pickling. # def __repr__(self): # for pickling purposes # if self.colTraverser: # self.colTraverser = None # # dictrepr = dict.__repr__(self.__dict__) # dictrepr = '%r(%r)' % (type(self).__name__, dictrepr) # print dictrepr # REMOVE # return dictrepr def watchMouseColl(self, task): """ This exists for debugging purposes to perpetually watch mouse collisions. """ # TODO make this highlight objects under the mouse for predictable object selection/grabber operations self.colTraverser.showCollisions(render) if base.mouseWatcherNode.hasMouse() and False == self.isCameraControlOn: # This gives the screen coordinates of the mouse. mPos = base.mouseWatcherNode.getMouse() # This makes the ray's origin the camera and makes the ray point # to the screen coordinates of the mouse. self.grabberRay.setFromLens(base.camNode, mPos.getX(), mPos.getY()) # traverses the graph for collisions self.colTraverser.traverse(render) return task.cont def scaleGrabber(self, task): if self.grabModelNP.isHidden(): return task.cont coreLst = self.grabModelNP.getPythonTag('grabberRoot') camPos = self.grabModelNP.getRelativePoint(self.grabModelNP, camera.getPos()) if camPos.z >= 0: # 1-4 if camPos.x > 0.0 <= camPos.y: # quad 1 coreLst[12].getParent().setScale( 1, 1, -1) elif camPos.x < 0.0 <= camPos.y: # quad 2 coreLst[12].getParent().setScale( -1, 1, -1) elif camPos.x < 0.0 >= camPos.y: # quad 3 coreLst[12].getParent().setScale( -1, -1, -1) elif camPos.x > 0.0 >= camPos.y: # quad 4 coreLst[12].getParent().setScale( 1, -1, -1) else: self.notify.warning("if-else default, scaleGrabber cam.z > 0") else: # 5-8 if camPos.x > 0.0 <= camPos.y: # quad 5 coreLst[12].getParent().setScale( 1, 1, 1) elif camPos.x < 0.0 <= camPos.y: # quad 6 coreLst[12].getParent().setScale( -1, 1, 1) elif camPos.x < 0.0 >= camPos.y: # quad 7 coreLst[12].getParent().setScale( -1, -1, 1) elif camPos.x > 0.0 >= camPos.y: # quad 8 coreLst[12].getParent().setScale( 1, -1, 1) else: self.notify.warning("if-else default, scaleGrabber cam.z z < 0") distToCam = (camera.getPos() - render.getRelativePoint(BBGlobalVars.currCoordSysNP, self.grabModelNP.getPos())).length() self.grabModelNP.setScale(self.grabScaleFactor * distToCam, self.grabScaleFactor * distToCam, self.grabScaleFactor * distToCam) # keep the position identical to the selection # for when outside objects like undo/redo move selected self.grabModelNP.setPos(render, self.selected.getPos(render)) return task.cont # TODO find a way to move camera control to a proper camera handler, perhaps move these to a global def enableCamera(self): self.isCameraControlOn = True PanditorEnableMouseFunc() def disableCamera(self): self.isCameraControlOn = False PanditorDisableMouseFunc() def handleM3(self): """Deselect the selected object.""" if not self.grabModelNP.isHidden() and not self.isCameraControlOn: # if the grab model is in the scene and the camera is not in control if base.mouseWatcherNode.hasMouse() and not self.isDragging: # we're ignoring accidental mouse3 clicks while dragging here with not isDragging self.selected = None # empty the selected, will be turned back on once something's selected messenger.ignore('mouse3', self) # turn the deselect event off self.grabModelNP.hide() # hide the grab model and set it back to render's pos self.grabModelNP.setPos(0.0) def handleM1Up(self): """Stop dragging the selected object.""" taskMgr.remove('mouse1Dragging') self.isDragging = False self.currTransformOperation = None # NOTE other references have been added, but no other object references them # record the mouse1 operation BBGlobalVars.undoHandler.record(self.selected, CommandUndo([self.initialCommandTrgVal], self.selected.setMat, self.selected.getMat(render))) messenger.ignore('mouse1-up', self) def handleM1(self): """Decides how to handle a mouse1 click.""" if self.isCameraControlOn: return if base.mouseWatcherNode.hasMouse(): # give the grabber first chance if self.grabModelNP.isHidden(): # no collisions w/ grabber or nothing selected # handle selection with scene objects self.handleSelection() elif not self.isDragging: # The grabber is in place but not dragging. Get ready to drag. self.handleManipulationSetup() # it'll call self.handleSelection() if no collision w/ grabber # TODO (if warranted) make self.handleManipulationSetup() return false if no col w/ grabber, call selection here instead def handleManipulationSetup(self): """Sets up all the attributes needed for the mouse dragging task.""" # This makes the ray's origin the camera and makes the ray point # to the screen coordinates of the mouse. if self.isDragging: return camVec = self.grabModelNP.getRelativeVector(self.grabModelNP, camera.getPos()) mPos = base.mouseWatcherNode.getMouse() self.grabberRay.setFromLens(base.camNode, mPos.getX(), mPos.getY()) self.colTraverser.traverse(self.grabModelNP) # look for collisions on the grabber if not self.isCameraControlOn and self.colHandlerQueue.getNumEntries() > 0 and not self.grabModelNP.isHidden(): # see if collided with the grabber if not handle re or multi selection self.colHandlerQueue.sortEntries() grabberObj = self.colHandlerQueue.getEntry(0).getIntoNodePath() grabberLst = self.grabModelNP.getPythonTag('grabberRoot') # see __init__ # the index gives the operations rot < 3 scale < 6 trans < 9 trans2D < 12 # mod index gives axis 0 == x, 1 == y, 2 == z ind = -1 for i in range(0, 13): if grabberObj == grabberLst[i]: ind = i grabberObj = grabberLst[i] # ensure we are not picking ourselves, ahem, the grabber assert(not self.grabModelNP.isAncestorOf(self.selected)) mPos3D = Point3(0.0) xVec = Vec3(1, 0, 0) yVec = Vec3(0, 1, 0) zVec = Vec3(0, 0, 1) # TODO: ??? break this up into translate rotate and scale function to make it readable if -1 < ind < 3: # rotate if ind % 3 == 0: # x self.initializeManipVars(Point3(1.0, 0.0, 0.0), self.transformOpEnum.rot, Point3(mPos.getX(), mPos.getY(), 0.0)) elif ind % 3 == 1: # y self.initializeManipVars(Point3(0.0, 1.0, 0.0), self.transformOpEnum.rot, Point3(mPos.getX(), mPos.getY(), 0.0)) else: # z self.initializeManipVars(Point3(0.0, 0.0, 1.0), self.transformOpEnum.rot, Point3(mPos.getX(), mPos.getY(), 0.0)) elif ind < 6: # scale if ind % 3 == 0: # x self.initializeManipVars(Point3(1.0, 0.0, 0.0), self.transformOpEnum.scale, Point3(mPos.getX(), mPos.getY(), 0.0)) elif ind % 3 == 1: # y # self.currTransformDir = Point3( 0.0, 1.0, 0.0) self.initializeManipVars(Point3(0.0, 1.0, 0.0), self.transformOpEnum.scale, Point3(mPos.getX(), mPos.getY(), 0.0)) else: # z # self.currTransformDir = Point3( 0.0, 0.0, 1.0) self.initializeManipVars(Point3(0.0, 0.0, 1.0), self.transformOpEnum.scale, Point3(mPos.getX(), mPos.getY(), 0.0)) elif ind < 9: # translate if ind % 3 == 0: # x # if the camera's too flat to the collision plane bad things happen if camVec.angleDeg( zVec) < 89.0 and self.getMousePlaneIntersect(mPos3D, zVec): self.initializeManipVars(Point3(1.0, 0.0, 0.0), self.transformOpEnum.trans, mPos3D, zVec) elif self.getMousePlaneIntersect(mPos3D, yVec): self.initializeManipVars(Point3(1.0, 0.0, 0.0), self.transformOpEnum.trans, mPos3D, yVec) elif ind % 3 == 1: # y if camVec.angleDeg( zVec) < 89.0 and self.getMousePlaneIntersect(mPos3D, zVec): self.initializeManipVars(Point3(0.0, 1.0, 0.0), self.transformOpEnum.trans, mPos3D, zVec) elif self.getMousePlaneIntersect(mPos3D, xVec): self.initializeManipVars(Point3(0.0, 1.0, 0.0), self.transformOpEnum.trans, mPos3D, xVec) else: # z if camVec.angleDeg( yVec) < 89.0 and self.getMousePlaneIntersect(mPos3D, yVec): self.initializeManipVars(Point3(0.0, 0.0, 1.0), self.transformOpEnum.trans, mPos3D, yVec) elif self.getMousePlaneIntersect(mPos3D, xVec): self.initializeManipVars(Point3(0.0, 0.0, 1.0), self.transformOpEnum.trans, mPos3D, xVec) elif ind < 12: # translate 2D if ind % 3 == 0: # xy if self.getMousePlaneIntersect(mPos3D, zVec): self.initializeManipVars(Point3(1.0, 1.0, 0.0), self.transformOpEnum.trans, mPos3D, zVec) elif ind % 3 == 1: # xz if self.getMousePlaneIntersect(mPos3D, yVec): self.initializeManipVars(Point3(1.0, 0.0, 1.0), self.transformOpEnum.trans, mPos3D, yVec) else: # zy if self.getMousePlaneIntersect(mPos3D, xVec): self.initializeManipVars(Point3(0.0, 1.0, 1.0), self.transformOpEnum.trans, mPos3D, xVec) elif ind == 12: # scale in three directions self.initializeManipVars(Point3(1.0, 1.0, 1.0), self.transformOpEnum.scale, Point3(mPos.getX(), mPos.getY(), 0.0)) else: self.notify.warning("Grabber Err: no grabber collision when col entries > 0 AND grabber not hidden") # Save initial value for save/undo. # The end result of the operation is sent to the undo handler on mouse up event. if self.selected: self.initialCommandTrgVal = self.selected.getMat(render) else: # no collisions w/ grabber or nothing selected # handle reselection or multi-selection (not yet implemented) with other scene obj self.handleSelection() def handleSelection(self): if self.isDragging: return # First check that the mouse is not outside the screen. if base.mouseWatcherNode.hasMouse() and False == self.isCameraControlOn: self.grabberColNode.setFromCollideMask(self.defaultBitMask) # This gives the screen coordinates of the mouse. mPos = base.mouseWatcherNode.getMouse() # This makes the ray's origin the camera and makes the ray point # to the screen coordinates of the mouse. self.colHandlerQueue.clearEntries() self.grabberRay.setFromLens(base.camNode, mPos.getX(), mPos.getY()) self.colTraverser.traverse(render) # look for collisions if self.colHandlerQueue.getNumEntries() > 0: self.colHandlerQueue.sortEntries() grabbedObj = self.colHandlerQueue.getEntry(0).getIntoNodePath() if not grabbedObj.findNetTag('pickable').isEmpty(): grabbedObj = grabbedObj.findNetTag('pickable') self.selected = grabbedObj self.grabModelNP.setPos(render, grabbedObj.getPos(render).x, grabbedObj.getPos(render).y, grabbedObj.getPos(render).z) self.grabModelNP.show() messenger.accept('mouse3', self, self.handleM3) def handleDragging(self, task): """ Does the actual work of manipulating objects, once the needed attributes have been setup by handleManipulationSetup(). """ if not self.isDragging: return task.done mPos3D = Point3(0.0) # # This section handles the actual translating rotating or scale after it's been set up in mouse1SetupManip...() # ONLY one operation is preformed per frame if self.currTransformOperation == self.transformOpEnum.trans: # 1st translation, rotation's section is at next elif if self.getMousePlaneIntersect(mPos3D, self.currPlaneColNorm): # get the difference between the last mouse and this frames mouse selectedNewPos = mPos3D - self.interFrameMousePosition # store this frames mouse self.interFrameMousePosition = mPos3D # add the difference to the selected object's pos self.selected.setPos(render, self.selected.getPos(render).x + self.currTransformDir.x * selectedNewPos.x, self.selected.getPos(render).y + self.currTransformDir.y * selectedNewPos.y, self.selected.getPos(render).z + self.currTransformDir.z * selectedNewPos.z) self.grabModelNP.setPos(render, self.selected.getPos(render)) elif self.currTransformOperation == self.transformOpEnum.rot: # 2nd rotation, followed finally by scaling # if operating on the z-axis, use the y (vertical screen coordinates otherwise use x (horizontal) mPos = base.mouseWatcherNode.getMouse() #rotMag = 0.0 if self.currTransformDir == Vec3( 0.0, 0.0, 1.0): rotMag = (mPos.x - self.interFrameMousePosition.x) * 1000 else: rotMag = (self.interFrameMousePosition.y - mPos.y) * 1000 initPos = self.selected.getPos() initPar = self.selected.getParent() self.selected.wrtReparentTo(render) self.selected.setMat(self.selected.getMat() * Mat4.rotateMat(rotMag, self.currTransformDir)) self.selected.wrtReparentTo(initPar) self.selected.setPos(initPos) self.interFrameMousePosition = Point3(mPos.x, mPos.y, 0.0) elif self.currTransformOperation == self.transformOpEnum.scale: # 3rd and final is scaling mPos = base.mouseWatcherNode.getMouse() # TODO: make dragging away from the object larger and to the object smaller (not simply left right up down) # td The problem with this MAY come if negative, mirrored, scaling is implemented. # if operating on the z-axis, use the y (vertical screen coordinates otherwise use x (horizontal) if self.currTransformDir == Point3( 0.0, 0.0, 1.0): sclMag = (mPos.y - self.interFrameMousePosition.y) * 5.5 elif self.currTransformDir == Point3( 0.0, 1.0, 0.0): sclMag = (mPos.x - self.interFrameMousePosition.x) * 5.5 else: sclMag = (self.interFrameMousePosition.x - mPos.x) * 5.5 # This is the line that prevents scaling past the origin. Flipping the faces doesn't seem to work. if -0.0001 < sclMag < 0.0001: sclMag = 0.000001 # create a dummy node to parent to and position such that applying scale to it will scale selected properly dummy = self.levitorNP.attachNewNode('dummy') initScl = dummy.getScale() # Don't forget the parent. Selected needs put back in place initPar = self.selected.getParent() initPos = self.selected.getPos() self.selected.wrtReparentTo(dummy) dummy.setScale(initScl.x + sclMag * self.currTransformDir.x, initScl.y + sclMag * self.currTransformDir.y, initScl.z + sclMag * self.currTransformDir.z) # reset selected's parent then destroy dummy self.selected.wrtReparentTo(initPar) self.selected.setPos(initPos) dummy.removeNode() dummy = None self.interFrameMousePosition = Point3( mPos.x, mPos.y, 0.0) else: self.notify.error("Err: Dragging with invalid curTransformOperation enum in handleDragging") return task.cont # ended by handleM1Up(), the mouse1-up event handler def initializeManipVars(self, transformDir, transformOp, mPos3D, planeNormVec=None): self.currTransformDir = transformDir self.currPlaneColNorm = planeNormVec # set the norm for the collision plane to be used in mouse1Dragging self.interFrameMousePosition = mPos3D self.currTransformOperation = transformOp self.isDragging = True taskMgr.add(self.handleDragging, 'mouse1Dragging') messenger.accept('mouse1-up', self, self.handleM1Up) def getMousePlaneIntersect(self, mPos3Dref, normVec): mPos = base.mouseWatcherNode.getMouse() plane = Plane(normVec, self.grabModelNP.getPos()) nearPoint = Point3() farPoint = Point3() base.camLens.extrude(mPos, nearPoint, farPoint) if plane.intersectsLine(mPos3Dref, render.getRelativePoint(camera, nearPoint), render.getRelativePoint(camera, farPoint)): return True return False def destroy(self): raise NotImplementedError('Make sure messenger etc are cleared of refs and the model node is deleted') self.grabModelNP.clearPythonTag('grabberRoot') self.grabModelNP.clearPythonTag('grabber') self.grabModelNP = None messenger.ignoreAll(self)
class Viewport(QtWidgets.QWidget, DirectObject): ClearColor = LEGlobals.vec3GammaToLinear(Vec4(0.361, 0.361, 0.361, 1.0)) def __init__(self, vpType, window, doc): DirectObject.__init__(self) QtWidgets.QWidget.__init__(self, window) self.doc = doc self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setMouseTracking(True) self.qtWindow = None self.qtWidget = None self.window = window self.type = vpType self.spec = VIEWPORT_SPECS[self.type] self.lens = None self.camNode = None self.camera = None self.cam = None self.win = None self.displayRegion = None self.mouseWatcher = None self.mouseWatcherNp = None self.buttonThrower = None self.clickRay = None self.clickNode = None self.clickNp = None self.clickQueue = None self.tickTask = None self.zoom = 1.0 self.gizmo = None self.inputDevice = None self.mouseAndKeyboard = None self.lastRenderTime = 0.0 self.enabled = False self.needsUpdate = True # 2D stuff copied from ShowBase :( self.camera2d = None self.cam2d = None self.render2d = None self.aspect2d = None self.a2dBackground = None self.a2dTop = None self.a2dBottom = None self.a2dLeft = None self.a2dRight = None self.a2dTopCenter = None self.a2dTopCenterNs = None self.a2dBottomCenter = None self.a2dBottomCenterNs = None self.a2dRightCenter = None self.a2dRightCenterNs = None self.a2dTopLeft = None self.a2dTopLeftNs = None self.a2dTopRight = None self.a2dTopRightNs = None self.a2dBottomLeft = None self.a2dBottomLeftNs = None self.a2dBottomRight = None self.a2dBottomRightNs = None self.__oldAspectRatio = None self.gridRoot = self.doc.render.attachNewNode("gridRoot") self.gridRoot.setLightOff(1) #self.gridRoot.setBSPMaterial("phase_14/materials/unlit.mat") #self.gridRoot.setDepthWrite(False) self.gridRoot.setBin("background", 0) self.gridRoot.hide(~self.getViewportMask()) self.grid = None def updateView(self, now=False): if now: self.renderView() else: self.needsUpdate = True def getGizmoAxes(self): raise NotImplementedError def getMouseRay(self, collRay=False): ray = CollisionRay() ray.setFromLens(self.camNode, self.getMouse()) if collRay: return ray else: return Ray(ray.getOrigin(), ray.getDirection()) def hasMouse(self): return self.mouseWatcher.hasMouse() def getMouse(self): if self.mouseWatcher.hasMouse(): return self.mouseWatcher.getMouse() return Point2(0, 0) def is3D(self): return self.type == VIEWPORT_3D def is2D(self): return self.type != VIEWPORT_3D def makeGrid(self): raise NotImplementedError def getViewportMask(self): return BitMask32.bit(self.type) def getViewportFullMask(self): return self.getViewportMask() def makeLens(self): raise NotImplementedError def getGridAxes(self): raise NotImplementedError def expand(self, point): return point def initialize(self): self.lens = self.makeLens() self.camera = self.doc.render.attachNewNode( ModelNode("viewportCameraParent")) self.camNode = Camera("viewportCamera") self.camNode.setLens(self.lens) self.camNode.setCameraMask(self.getViewportMask()) self.cam = self.camera.attachNewNode(self.camNode) winprops = WindowProperties.getDefault() winprops.setParentWindow(int(self.winId())) winprops.setForeground(False) winprops.setUndecorated(True) gsg = self.doc.gsg output = base.graphicsEngine.makeOutput( base.pipe, "viewportOutput", 0, FrameBufferProperties.getDefault(), winprops, (GraphicsPipe.BFFbPropsOptional | GraphicsPipe.BFRequireWindow), gsg) self.qtWindow = QtGui.QWindow.fromWinId( output.getWindowHandle().getIntHandle()) self.qtWidget = QtWidgets.QWidget.createWindowContainer( self.qtWindow, self, QtCore.Qt.WindowDoesNotAcceptFocus | QtCore.Qt.WindowTransparentForInput | QtCore.Qt.WindowStaysOnBottomHint | QtCore.Qt.BypassWindowManagerHint | QtCore.Qt.SubWindow) #, #(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowDoesNotAcceptFocus #| QtCore.Qt.WindowTransparentForInput | QtCore.Qt.BypassWindowManagerHint #| QtCore.Qt.SubWindow | QtCore.Qt.WindowStaysOnBottomHint)) self.qtWidget.setFocusPolicy(QtCore.Qt.NoFocus) self.inputDevice = output.getInputDevice(0) assert output is not None, "Unable to create viewport output!" dr = output.makeDisplayRegion() dr.disableClears() dr.setCamera(self.cam) self.displayRegion = dr output.disableClears() output.setClearColor(Viewport.ClearColor) output.setClearColorActive(True) output.setClearDepthActive(True) output.setActive(True) self.win = output # keep track of the mouse in this viewport mak = MouseAndKeyboard(self.win, 0, "mouse") mouse = base.dataRoot.attachNewNode(mak) self.mouseAndKeyboard = mouse self.mouseWatcher = MouseWatcher() self.mouseWatcher.setDisplayRegion(self.displayRegion) mw = mouse.attachNewNode(self.mouseWatcher) self.mouseWatcherNp = mw # listen for keyboard and mouse events in this viewport bt = ButtonThrower("kbEvents") bt.setButtonDownEvent("btndown") bt.setButtonUpEvent("btnup") mods = ModifierButtons() mods.addButton(KeyboardButton.shift()) mods.addButton(KeyboardButton.control()) mods.addButton(KeyboardButton.alt()) mods.addButton(KeyboardButton.meta()) bt.setModifierButtons(mods) self.buttonThrower = mouse.attachNewNode(bt) # collision objects for clicking on objects from this viewport self.clickRay = CollisionRay() self.clickNode = CollisionNode("viewportClickRay") self.clickNode.addSolid(self.clickRay) self.clickNp = NodePath(self.clickNode) self.clickQueue = CollisionHandlerQueue() self.setupRender2d() self.setupCamera2d() self.gizmo = ViewportGizmo(self) self.doc.viewportMgr.addViewport(self) self.makeGrid() def cleanup(self): self.grid.cleanup() self.grid = None self.gridRoot.removeNode() self.gridRoot = None self.lens = None self.camNode = None self.cam.removeNode() self.cam = None self.camera.removeNode() self.camera = None self.spec = None self.doc = None self.type = None self.window = None self.zoom = None self.gizmo.cleanup() self.gizmo = None self.clickNp.removeNode() self.clickNp = None self.clickQueue.clearEntries() self.clickQueue = None self.clickNode = None self.clickRay = None self.buttonThrower.removeNode() self.buttonThrower = None self.inputDevice = None self.mouseWatcherNp.removeNode() self.mouseWatcherNp = None self.mouseWatcher = None self.mouseAndKeyboard.removeNode() self.mouseAndKeyboard = None self.win.removeAllDisplayRegions() self.displayRegion = None base.graphicsEngine.removeWindow(self.win) self.win = None self.camera2d.removeNode() self.camera2d = None self.cam2d = None self.render2d.removeNode() self.render2d = None self.a2dBackground = None self.a2dTop = None self.a2dBottom = None self.a2dLeft = None self.a2dRight = None self.aspect2d = None self.a2dTopCenter = None self.a2dTopCenterNs = None self.a2dBottomCenter = None self.a2dBottomCenterNs = None self.a2dLeftCenter = None self.a2dLeftCenterNs = None self.a2dRightCenter = None self.a2dRightCenterNs = None self.a2dTopLeft = None self.a2dTopLeftNs = None self.a2dTopRight = None self.a2dTopRightNs = None self.a2dBottomLeft = None self.a2dBottomLeftNs = None self.a2dBottomRight = None self.a2dBottomRightNs = None self.__oldAspectRatio = None self.qtWindow.deleteLater() self.qtWidget.deleteLater() self.qtWindow = None self.qtWidget = None self.deleteLater() def keyPressEvent(self, event): button = LEUtils.keyboardButtonFromQtKey(event.key()) if button: self.inputDevice.buttonDown(button) def keyReleaseEvent(self, event): button = LEUtils.keyboardButtonFromQtKey(event.key()) if button: self.inputDevice.buttonUp(button) def enterEvent(self, event): # Give ourselves focus. self.setFocus() QtWidgets.QWidget.enterEvent(self, event) def mouseMoveEvent(self, event): self.inputDevice.setPointerInWindow(event.pos().x(), event.pos().y()) QtWidgets.QWidget.mouseMoveEvent(self, event) def leaveEvent(self, event): self.clearFocus() self.inputDevice.setPointerOutOfWindow() self.inputDevice.focusLost() QtWidgets.QWidget.leaveEvent(self, event) def mousePressEvent(self, event): btn = event.button() if btn == QtCore.Qt.LeftButton: self.inputDevice.buttonDown(MouseButton.one()) elif btn == QtCore.Qt.MiddleButton: self.inputDevice.buttonDown(MouseButton.two()) elif btn == QtCore.Qt.RightButton: self.inputDevice.buttonDown(MouseButton.three()) QtWidgets.QWidget.mousePressEvent(self, event) def mouseReleaseEvent(self, event): btn = event.button() if btn == QtCore.Qt.LeftButton: self.inputDevice.buttonUp(MouseButton.one()) elif btn == QtCore.Qt.MiddleButton: self.inputDevice.buttonUp(MouseButton.two()) elif btn == QtCore.Qt.RightButton: self.inputDevice.buttonUp(MouseButton.three()) QtWidgets.QWidget.mouseReleaseEvent(self, event) def wheelEvent(self, event): ang = event.angleDelta().y() if ang > 0: self.inputDevice.buttonDown(MouseButton.wheelUp()) self.inputDevice.buttonUp(MouseButton.wheelUp()) else: self.inputDevice.buttonDown(MouseButton.wheelDown()) self.inputDevice.buttonUp(MouseButton.wheelDown()) QtWidgets.QWidget.wheelEvent(self, event) def getAspectRatio(self): return self.win.getXSize() / self.win.getYSize() def setupRender2d(self): ## This is the root of the 2-D scene graph. self.render2d = NodePath("viewport-render2d") # Set up some overrides to turn off certain properties which # we probably won't need for 2-d objects. # It's probably important to turn off the depth test, since # many 2-d objects will be drawn over each other without # regard to depth position. # We used to avoid clearing the depth buffer before drawing # render2d, but nowadays we clear it anyway, since we # occasionally want to put 3-d geometry under render2d, and # it's simplest (and seems to be easier on graphics drivers) # if the 2-d scene has been cleared first. self.render2d.setDepthTest(0) self.render2d.setDepthWrite(0) self.render2d.setMaterialOff(1) self.render2d.setTwoSided(1) self.aspect2d = self.render2d.attachNewNode("viewport-aspect2d") aspectRatio = self.getAspectRatio() self.aspect2d.setScale(1.0 / aspectRatio, 1.0, 1.0) self.a2dBackground = self.aspect2d.attachNewNode("a2dBackground") ## The Z position of the top border of the aspect2d screen. self.a2dTop = 1.0 ## The Z position of the bottom border of the aspect2d screen. self.a2dBottom = -1.0 ## The X position of the left border of the aspect2d screen. self.a2dLeft = -aspectRatio ## The X position of the right border of the aspect2d screen. self.a2dRight = aspectRatio self.a2dTopCenter = self.aspect2d.attachNewNode("a2dTopCenter") self.a2dTopCenterNs = self.aspect2d.attachNewNode("a2dTopCenterNS") self.a2dBottomCenter = self.aspect2d.attachNewNode("a2dBottomCenter") self.a2dBottomCenterNs = self.aspect2d.attachNewNode( "a2dBottomCenterNS") self.a2dLeftCenter = self.aspect2d.attachNewNode("a2dLeftCenter") self.a2dLeftCenterNs = self.aspect2d.attachNewNode("a2dLeftCenterNS") self.a2dRightCenter = self.aspect2d.attachNewNode("a2dRightCenter") self.a2dRightCenterNs = self.aspect2d.attachNewNode("a2dRightCenterNS") self.a2dTopLeft = self.aspect2d.attachNewNode("a2dTopLeft") self.a2dTopLeftNs = self.aspect2d.attachNewNode("a2dTopLeftNS") self.a2dTopRight = self.aspect2d.attachNewNode("a2dTopRight") self.a2dTopRightNs = self.aspect2d.attachNewNode("a2dTopRightNS") self.a2dBottomLeft = self.aspect2d.attachNewNode("a2dBottomLeft") self.a2dBottomLeftNs = self.aspect2d.attachNewNode("a2dBottomLeftNS") self.a2dBottomRight = self.aspect2d.attachNewNode("a2dBottomRight") self.a2dBottomRightNs = self.aspect2d.attachNewNode("a2dBottomRightNS") # Put the nodes in their places self.a2dTopCenter.setPos(0, 0, self.a2dTop) self.a2dTopCenterNs.setPos(0, 0, self.a2dTop) self.a2dBottomCenter.setPos(0, 0, self.a2dBottom) self.a2dBottomCenterNs.setPos(0, 0, self.a2dBottom) self.a2dLeftCenter.setPos(self.a2dLeft, 0, 0) self.a2dLeftCenterNs.setPos(self.a2dLeft, 0, 0) self.a2dRightCenter.setPos(self.a2dRight, 0, 0) self.a2dRightCenterNs.setPos(self.a2dRight, 0, 0) self.a2dTopLeft.setPos(self.a2dLeft, 0, self.a2dTop) self.a2dTopLeftNs.setPos(self.a2dLeft, 0, self.a2dTop) self.a2dTopRight.setPos(self.a2dRight, 0, self.a2dTop) self.a2dTopRightNs.setPos(self.a2dRight, 0, self.a2dTop) self.a2dBottomLeft.setPos(self.a2dLeft, 0, self.a2dBottom) self.a2dBottomLeftNs.setPos(self.a2dLeft, 0, self.a2dBottom) self.a2dBottomRight.setPos(self.a2dRight, 0, self.a2dBottom) self.a2dBottomRightNs.setPos(self.a2dRight, 0, self.a2dBottom) def setupCamera2d(self, sort=10, displayRegion=(0, 1, 0, 1), coords=(-1, 1, -1, 1)): dr = self.win.makeMonoDisplayRegion(*displayRegion) dr.setSort(10) # Enable clearing of the depth buffer on this new display # region (see the comment in setupRender2d, above). dr.setClearDepthActive(1) # Make any texture reloads on the gui come up immediately. dr.setIncompleteRender(False) left, right, bottom, top = coords # Now make a new Camera node. cam2dNode = Camera('cam2d') lens = OrthographicLens() lens.setFilmSize(right - left, top - bottom) lens.setFilmOffset((right + left) * 0.5, (top + bottom) * 0.5) lens.setNearFar(-1000, 1000) cam2dNode.setLens(lens) # self.camera2d is the analog of self.camera, although it's # not as clear how useful it is. self.camera2d = self.render2d.attachNewNode('camera2d') camera2d = self.camera2d.attachNewNode(cam2dNode) dr.setCamera(camera2d) self.cam2d = camera2d return camera2d def mouse1Up(self): pass def mouse1Down(self): pass def mouse2Up(self): pass def mouse2Down(self): pass def mouse3Up(self): pass def mouse3Down(self): pass def mouseEnter(self): self.updateView() def mouseExit(self): pass def mouseMove(self): pass def wheelUp(self): pass def wheelDown(self): pass def shouldRender(self): if not self.enabled: return False now = globalClock.getRealTime() if self.lastRenderTime != 0: elapsed = now - self.lastRenderTime if elapsed <= 0: return False frameRate = 1 / elapsed if frameRate > 100.0: # Never render faster than 100Hz return False return self.needsUpdate def renderView(self): self.lastRenderTime = globalClock.getRealTime() self.needsUpdate = False #self.win.setActive(1) base.requestRender() def tick(self): if self.shouldRender(): self.renderView() else: pass #self.win.setActive(0) def getViewportName(self): return self.spec.name def getViewportCenterPixels(self): return LPoint2i(self.win.getXSize() // 2, self.win.getYSize() // 2) def centerCursor(self, cursor): center = self.getViewportCenterPixels() cursor.setPos( self.mapToGlobal(QtCore.QPoint(self.width() / 2, self.height() / 2))) self.inputDevice.setPointerInWindow(center.x, center.y) def viewportToWorld(self, viewport, vec=False): front = Point3() back = Point3() self.lens.extrude(viewport, front, back) world = (front + back) / 2 worldMat = self.cam.getMat(render) if vec: world = worldMat.xformVec(world) else: world = worldMat.xformPoint(world) return world def worldToViewport(self, world): # move into local camera space invMat = Mat4(self.cam.getMat(render)) invMat.invertInPlace() local = invMat.xformPoint(world) point = Point2() self.lens.project(local, point) return point def zeroUnusedCoordinate(self, vec): pass def click(self, mask, queue=None, traverser=None, root=None): if not self.mouseWatcher.hasMouse(): return None if not queue: queue = self.clickQueue self.clickRay.setFromLens(self.camNode, self.mouseWatcher.getMouse()) self.clickNode.setFromCollideMask(mask) self.clickNode.setIntoCollideMask(BitMask32.allOff()) self.clickNp.reparentTo(self.cam) queue.clearEntries() if not traverser: base.clickTraverse(self.clickNp, queue) else: if not root: root = self.doc.render traverser.addCollider(self.clickNp, queue) traverser.traverse(root) traverser.removeCollider(self.clickNp) queue.sortEntries() self.clickNp.reparentTo(NodePath()) return queue.getEntries() def fixRatio(self, size=None): if not self.lens: return if size is None: aspectRatio = self.win.getXSize() / self.win.getYSize() else: if size.y > 0: aspectRatio = size.x / size.y else: aspectRatio = 1.0 if self.is2D(): zoomFactor = (1.0 / self.zoom) * 100.0 self.lens.setFilmSize(zoomFactor * aspectRatio, zoomFactor) else: self.lens.setAspectRatio(aspectRatio) if aspectRatio != self.__oldAspectRatio: self.__oldAspectRatio = aspectRatio # Fix up some anything that depends on the aspectRatio if aspectRatio < 1: # If the window is TALL, lets expand the top and bottom self.aspect2d.setScale(1.0, aspectRatio, aspectRatio) self.a2dTop = 1.0 / aspectRatio self.a2dBottom = -1.0 / aspectRatio self.a2dLeft = -1 self.a2dRight = 1.0 else: # If the window is WIDE, lets expand the left and right self.aspect2d.setScale(1.0 / aspectRatio, 1.0, 1.0) self.a2dTop = 1.0 self.a2dBottom = -1.0 self.a2dLeft = -aspectRatio self.a2dRight = aspectRatio # Reposition the aspect2d marker nodes self.a2dTopCenter.setPos(0, 0, self.a2dTop) self.a2dTopCenterNs.setPos(0, 0, self.a2dTop) self.a2dBottomCenter.setPos(0, 0, self.a2dBottom) self.a2dBottomCenterNs.setPos(0, 0, self.a2dBottom) self.a2dLeftCenter.setPos(self.a2dLeft, 0, 0) self.a2dLeftCenterNs.setPos(self.a2dLeft, 0, 0) self.a2dRightCenter.setPos(self.a2dRight, 0, 0) self.a2dRightCenterNs.setPos(self.a2dRight, 0, 0) self.a2dTopLeft.setPos(self.a2dLeft, 0, self.a2dTop) self.a2dTopLeftNs.setPos(self.a2dLeft, 0, self.a2dTop) self.a2dTopRight.setPos(self.a2dRight, 0, self.a2dTop) self.a2dTopRightNs.setPos(self.a2dRight, 0, self.a2dTop) self.a2dBottomLeft.setPos(self.a2dLeft, 0, self.a2dBottom) self.a2dBottomLeftNs.setPos(self.a2dLeft, 0, self.a2dBottom) self.a2dBottomRight.setPos(self.a2dRight, 0, self.a2dBottom) self.a2dBottomRightNs.setPos(self.a2dRight, 0, self.a2dBottom) def resizeEvent(self, event): if not self.win: return newsize = LVector2i(event.size().width(), event.size().height()) self.qtWidget.resize(newsize[0], newsize[1]) self.qtWidget.move(0, 0) #props = WindowProperties() #props.setSize(newsize) #props.setOrigin(0, 0) #self.win.requestProperties(props) self.fixRatio(newsize) self.onResize(newsize) self.updateView() def onResize(self, newsize): pass def draw(self): pass def enable(self): # Render to the viewport self.win.setActive(True) self.enabled = True def disable(self): # Don't render to the viewport self.win.setActive(False) self.enabled = False
class MapPicker(): __name: Final[str] __base: Final[ShowBase] __data: Final[NDArray[(Any, Any, Any), np.uint8]] # collision data __ctrav: Final[CollisionTraverser] __cqueue: Final[CollisionHandlerQueue] __cn: Final[CollisionNode] __cnp: Final[NodePath] # picker data __pn: Final[CollisionNode] __pnp: Final[NodePath] __pray: Final[CollisionRay] # constants COLLIDE_MASK: Final[BitMask32] = BitMask32.bit(1) def __init__(self, services: Services, base: ShowBase, map_data: MapData, name: Optional[str] = None): self.__services = services self.__services.ev_manager.register_listener(self) self.__base = base self.__name = name if name is not None else (map_data.name + "_picker") self.__map = map_data self.__data = map_data.data # collision traverser & queue self.__ctrav = CollisionTraverser(self.name + '_ctrav') self.__cqueue = CollisionHandlerQueue() # collision boxes self.__cn = CollisionNode(self.name + '_cn') self.__cn.set_collide_mask(MapPicker.COLLIDE_MASK) self.__cnp = self.__map.root.attach_new_node(self.__cn) self.__ctrav.add_collider(self.__cnp, self.__cqueue) self.__points = [] z_offset = 1 if self.__map.dim == 3 else self.__map.depth for idx in np.ndindex(self.__data.shape): if bool(self.__data[idx] & MapData.TRAVERSABLE_MASK): p = Point(*idx) self.__points.append(p) idx = self.__cn.add_solid(CollisionBox(idx, Point3(p.x+1, p.y+1, p.z-z_offset))) assert idx == (len(self.__points) - 1) # mouse picker self.__pn = CollisionNode(self.name + '_pray') self.__pnp = self.__base.cam.attach_new_node(self.__pn) self.__pn.set_from_collide_mask(MapPicker.COLLIDE_MASK) self.__pray = CollisionRay() self.__pn.add_solid(self.__pray) self.__ctrav.add_collider(self.__pnp, self.__cqueue) # debug -> shows collision ray / impact point # self.__ctrav.show_collisions(self.__map.root) @property def name(self) -> str: return self.__name @property def pos(self): # check if we have access to the mouse if not self.__base.mouseWatcherNode.hasMouse(): return None # get the mouse position mpos = self.__base.mouseWatcherNode.get_mouse() # set the position of the ray based on the mouse position self.__pray.set_from_lens(self.__base.camNode, mpos.getX(), mpos.getY()) # find collisions self.__ctrav.traverse(self.__map.root) # if we have hit something sort the hits so that the closest is first if self.__cqueue.get_num_entries() == 0: return None self.__cqueue.sort_entries() # compute & return logical cube position x, y, z = self.__cqueue.get_entry(0).getSurfacePoint(self.__map.root) x, y, z = [max(math.floor(x), 0), max(math.floor(y), 0), max(math.ceil(z), 0)] if x == len(self.__data): x -= 1 if y == len(self.__data[x]): y -= 1 if z == len(self.__data[x][y]): z -= 1 return Point(x, y, z) def notify(self, event: Event) -> None: if isinstance(event, MapUpdateEvent): z_offset = 1 if self.__map.dim == 3 else self.__map.depth for p in event.updated_cells: if p.n_dim == 2: p = Point(*p, 0) if bool(self.__data[p.values] & MapData.TRAVERSABLE_MASK): self.__points.append(p) idx = self.__cn.add_solid(CollisionBox(p.values, Point3(p.x+1, p.y+1, p.z-z_offset))) assert idx == (len(self.__points) - 1) else: try: i = self.__points.index(p) except ValueError: continue self.__cn.remove_solid(i) self.__points.pop(i) def destroy(self) -> None: self.__cqueue.clearEntries() self.__ctrav.clear_colliders() self.__cnp.remove_node() self.__pnp.remove_node()
class PositionExaminer(DirectObject, NodePath): def __init__(self): try: self.__initialized return except: self.__initialized = 1 NodePath.__init__(self, hidden.attachNewNode('PositionExaminer')) self.cRay = CollisionRay(0.0, 0.0, 6.0, 0.0, 0.0, -1.0) self.cRayNode = CollisionNode('cRayNode') self.cRayNode.addSolid(self.cRay) self.cRayNodePath = self.attachNewNode(self.cRayNode) self.cRayNodePath.hide() self.cRayBitMask = CIGlobals.FloorBitmask self.cRayNode.setFromCollideMask(self.cRayBitMask) self.cRayNode.setIntoCollideMask(BitMask32.allOff()) self.cSphere = CollisionSphere(0.0, 0.0, 0.0, 1.5) self.cSphereNode = CollisionNode('cSphereNode') self.cSphereNode.addSolid(self.cSphere) self.cSphereNodePath = self.attachNewNode(self.cSphereNode) self.cSphereNodePath.hide() self.cSphereBitMask = CIGlobals.WallBitmask self.cSphereNode.setFromCollideMask(self.cSphereBitMask) self.cSphereNode.setIntoCollideMask(BitMask32.allOff()) self.ccLine = CollisionSegment(0.0, 0.0, 0.0, 1.0, 0.0, 0.0) self.ccLineNode = CollisionNode('ccLineNode') self.ccLineNode.addSolid(self.ccLine) self.ccLineNodePath = self.attachNewNode(self.ccLineNode) self.ccLineNodePath.hide() self.ccLineBitMask = CIGlobals.CameraBitmask self.ccLineNode.setFromCollideMask(self.ccLineBitMask) self.ccLineNode.setIntoCollideMask(BitMask32.allOff()) self.cRayTrav = CollisionTraverser('PositionExaminer.cRayTrav') self.cRayTrav.setRespectPrevTransform(False) self.cRayQueue = CollisionHandlerQueue() self.cRayTrav.addCollider(self.cRayNodePath, self.cRayQueue) self.cSphereTrav = CollisionTraverser('PositionExaminer.cSphereTrav') self.cSphereTrav.setRespectPrevTransform(False) self.cSphereQueue = CollisionHandlerQueue() self.cSphereTrav.addCollider(self.cSphereNodePath, self.cSphereQueue) self.ccLineTrav = CollisionTraverser('PositionExaminer.ccLineTrav') self.ccLineTrav.setRespectPrevTransform(False) self.ccLineQueue = CollisionHandlerQueue() self.ccLineTrav.addCollider(self.ccLineNodePath, self.ccLineQueue) def delete(self): del self.cRay del self.cRayNode self.cRayNodePath.removeNode() del self.cRayNodePath del self.cSphere del self.cSphereNode self.cSphereNodePath.removeNode() del self.cSphereNodePath del self.ccLine del self.ccLineNode self.ccLineNodePath.removeNode() del self.ccLineNodePath del self.cRayTrav del self.cRayQueue del self.cSphereTrav del self.cSphereQueue del self.ccLineTrav del self.ccLineQueue def consider(self, node, pos, eyeHeight): self.reparentTo(node) self.setPos(pos) result = None self.cRayTrav.traverse(render) if self.cRayQueue.getNumEntries() != 0: self.cRayQueue.sortEntries() floorPoint = self.cRayQueue.getEntry(0).getSurfacePoint(self.cRayNodePath) if abs(floorPoint[2]) <= 4.0: pos += floorPoint self.setPos(pos) self.cSphereTrav.traverse(render) if self.cSphereQueue.getNumEntries() == 0: self.ccLine.setPointA(0, 0, eyeHeight) self.ccLine.setPointB(-pos[0], -pos[1], eyeHeight) self.ccLineTrav.traverse(render) if self.ccLineQueue.getNumEntries() == 0: result = pos self.reparentTo(hidden) self.cRayQueue.clearEntries() self.cSphereQueue.clearEntries() self.ccLineQueue.clearEntries() return result
class Hooded(AICharacter): SIGHT=7.5 FOV=60.0 HEIGHT = 1.3 STATE_PATROL = 0 STATE_SEARCH = 1 STATE_WANDER = 2 STATE_ATTACK = 3 STATE_PAUSED = 4 class StatePatrol: pass def __init__(self, name, root, route, mass, movforce, maxforce): AICharacter.__init__(self, name, root, mass, movforce, maxforce) self.state = Hooded.STATE_PATROL self.initTimer = True self.attackTimer = True # we create a spotlight that will be the sentinel's eye and will be used to fire the inView method self.slight = Spotlight('slight') self.slight.setColor((1, 1, 1, 1)) lens = PerspectiveLens() lens.setNear(0.1) lens.setFar(Hooded.SIGHT) lens.setFov(Hooded.FOV) self.slight.setLens(lens) self.slnp = self.get_node_path().attachNewNode(self.slight) #TODO: Substitute for a collision polygon, so that the player class alerts an enemy of its presence #self.slight.showFrustum() self.slnp.setH(self.slnp.getH()-180) self.slnp.setPos(0, 0, Hooded.HEIGHT) self.hearing = 5.0 self.dynamicObstacles = [] self.detected = False self.pathfinding = False self.lostTarget = False self.countTime = False self.goingBack = False self.heard = False self.isProtected = False self.attacked = False self.started = False self.sentinelHandler = CollisionHandlerQueue() #TODO: Intruders should be added via an external method self.intruders = [] # this is important: as we said the inView method don't cull geometry but take everything is in sight frustum - therefore to simulate an hide and seek feature we gotta cheat a little: this ray is masked to collide with walls and so if the avatar is behind a wall the ray will be 'deflected' (we'll see how later in the sent_traverse function) - so we know who's behind a wall but we fake we can't see it. sentraygeom = CollisionSegment(0, 0, Hooded.HEIGHT, 0, Hooded.SIGHT, Hooded.HEIGHT) sentinelRay = self.get_node_path().attachNewNode(CollisionNode('sentinelray')) sentinelRay.node().addSolid(sentraygeom) # we set to the ray a cumulative masking using the or operator to detect either the avatar's body and the wall geometry sentinelRay.node().setFromCollideMask(CollisionMask.PLAYER) sentinelRay.node().setIntoCollideMask(CollisionMask.NONE) # we add the ray to the sentinel collider and now it is ready to go base.cTrav.addCollider(sentinelRay, self.sentinelHandler) self.screechsound = loader.loadSfx("assets/sounds/enemies/nazgul_scream.mp3") self.setPatrolPos(route) def __del__(self): self.slnp.removeNode() def addIntruder(self, intruder): self.intruders.append(intruder) def setPatrolPos(self, route): self.currentTarget = 0 self.route = route self.numTargets = len(route) self.increment = 1 self.getAiBehaviors().seek(self.route[0]) #to update the AIWorld def update(self): if (self.started == False): return False captured = self.sent_detect() if (captured): if (self.isProtected): self.state = Hooded.STATE_PAUSED elif (self.state != Hooded.STATE_SEARCH): self.state = Hooded.STATE_SEARCH self.getAiBehaviors().pauseAi("all") self.lostTarget = False self.heard = False self.resetTimer() """elif (self.heard): self.heard = False self.pursueTarget = self.hearingPos self.getAiBehaviors().pauseAi("all") self.state = Hooded.STATE_SEARCH elif (self.state == Hooded.STATE_SEARCH and self.lostTarget == False and self.goingBack == False): self.startTimer(1.5) hasFinished = self.timer() if (hasFinished == True): self.lostTarget = True self.getAiBehaviors().pauseAi("all") #self.state = 4 self.pursueTarget = self.TargetPos else: self.lostTarget = False""" if self.state == Hooded.STATE_PATROL: self.patrol() elif self.state == Hooded.STATE_SEARCH: self.pursue() elif self.state == Hooded.STATE_WANDER: self.wander() elif self.state == Hooded.STATE_ATTACK: self.kill() elif self.state == Hooded.STATE_PAUSED: self.pause() #elif self.state == Hooded.STATE_SEARCH: #self.pathfind() if (self.attacked): self.attacked = False return True return False #TODO: Patrol is ready for refactoring, if needed PATROL_PAUSE = 3.0 PATROL_DISTANCE = 1.0 def patrol(self): distance = self.get_node_path().getDistance(self.route[self.currentTarget]) if (distance < Hooded.PATROL_DISTANCE): self.startTimer(Hooded.PATROL_PAUSE) self.getAiBehaviors().pauseAi("all") if self.timer(): self.currentTarget = (self.currentTarget + 1) % len(self.route) self.resetTimer() self.getAiBehaviors().pauseAi("all") self.getAiBehaviors().seek(self.route[self.currentTarget]) self.getAiBehaviors().resumeAi("seek") def pathfind(self): if (not self.getAiBehaviors().behaviorStatus("pathfollow") in ["active", "done"]): self.getAiBehaviors().initPathFind("assets/navmesh.csv") self.getAiBehaviors().pauseAi("all") if isinstance(self.pursueTarget, NodePath): self.getAiBehaviors().pathFindTo(self.pursueTarget) else: self.getAiBehaviors().pathFindTo(self.pursueTarget.getNodePath()) for i in self.dynamicObstacles: self.getAiBehaviors().addDynamicObstacle(i) #self.pathfinding = True currentPos = self.get_node_path().getPos(render) if (isinstance(self.pursueTarget, NodePath)): self.TargetPos = self.pursueTarget else: self.TargetPos = self.pursueTarget.getNodePath() # print currentPos, self.TargetPos distance = self.get_node_path().getDistance(self.TargetPos) if (self.getAiBehaviors().behaviorStatus("pathfollow") == "done"): if (distance > 5): self.getAiBehaviors().pauseAi("all") return if (self.lostTarget == False): if (self.goingBack == True): self.getAiBehaviors().pauseAi("all") self.getAiBehaviors().seek(self.route[self.currentTarget]) self.state = Hooded.STATE_PATROL self.getAiBehaviors().resumeAi("seek") self.resetTimer() self.goingBack = False elif (self.heard == True): if (isinstance(self.pursueTarget, NodePath)): self.getAiBehaviors().pauseAi("all") self.state = Hooded.STATE_ATTACK else: self.startTimer(5) self.countTime = True self.pathfinding = False self.state = Hooded.STATE_WANDER self.radius = 5 self.aoe = 10 else: self.getAiBehaviors().pauseAi("all") self.state = Hooded.STATE_ATTACK else: self.startTimer(5) self.countTime = True self.pathfinding = False self.state = Hooded.STATE_WANDER self.radius = 5 self.aoe = 10 def wander(self): if (self.getAiBehaviors().behaviorStatus("wander") != "active"): self.getAiBehaviors().pauseAi("all") self.getAiBehaviors().wander(self.radius, 0,self.aoe, 1.0) #self.getAiBehaviors().resumeAi("wander") if (self.lostTarget == True and self.countTime == True): self.startTimer(5) hasFinished = self.timer() if (hasFinished == True): self.currentTarget += self.increment if (self.currentTarget == self.numTargets - 1): self.increment = -1 else: if (self.currentTarget == 0): self.increment = 1 self.pursueTarget = self.route[self.currentTarget] self.state = Hooded.STATE_SEARCH self.goingBack = True self.lostTarget= False self.getAiBehaviors().pauseAi("all") #self.getAiBehaviors().seek(self.route[self.currentTarget]) def kill(self): if (self.attackTimer): self.attacked = True self.attackTimer = False self.startTimer(3) self.pause() else: hasFinished = self.timer() if (hasFinished): self.attackTimer = True self.resetTimer() self.state = Hooded.STATE_SEARCH else: self.attacked = False #TODO: Use event handling, instead? def sent_traverse(self, suspect): if (self.sentinelHandler.getNumEntries() > 0): self.sentinelHandler.sortEntries() # for i in range(self.sentinelHandler.getNumEntries()): # print self.sentinelHandler.getEntry(i) entry = self.sentinelHandler.getEntry(0) self.sentinelHandler.clearEntries() colliderNP = entry.getIntoNodePath() if colliderNP.getParent() == suspect.getNodePath(): # self = False if self.detected == False: self.detected = True self.screechsound.play() suspect.boo() return True #TODO: This was meant for the implementation of safe areas, where the player could not be reached elif colliderNP.getName() == 'lightarea': newEntry = self.sentinelHandler.getEntry(1) newColliderNode = newEntry.getIntoNode() if (newColliderNode.getName() == 'playercol'): #check if player is really inside light area self.isProtected = True return True return False #** Here then we'll unleash the power of isInView method - this function is just a query if a 3D point is inside its frustum so it works for objects with lens, such as cameras or even, as in this case, a spotlight. But to make this happen, we got cheat a little, knowing in advance who we're going to seek, to query its position afterwards, and that's what the next line is about: to collect all the references for objects named 'smiley' def sent_detect(self): for o in self.intruders: # query the spotlight if something listed as 'intruders' is-In-View at its position and if this is the case we'll call the traverse function above to see if is open air or hidden from the sentinel's sight if self.slnp.node().isInView(o.getNodePath().getPos(self.slnp)): self.get_node_path().lookAt(o.getNodePath()) if self.sent_traverse(o): self.pursueTarget = o return True return False def timer(self): currentTime = time.time() diff = currentTime - self.time if (diff > self.interval): self.initTimer = True return True else: return False def resetTimer(self): self.initTimer = True def startTimer(self, interval): if (self.initTimer == True): self.interval = interval self.initTimer = False self.time = time.time() def addDynamicObject(self, dynamicObject): self.dynamicObstacles.append(dynamicObject) def hear(self, noisePos): dist = self.get_node_path().getDistance(noisePos) if (dist <= self.hearing): self.heard = True self.hearingPos = noisePos def pause(self): self.getAiBehaviors().pauseAi("all") #print "nao pausei?" def start(self): self.started = True def stop(self): self.started = False def clean(self): loader.unloadSfx(self.screechsound) def pursue(self): self.getAiBehaviors().pursue(self.pursueTarget.getNodePath()) if (self.get_node_path().getDistance(self.pursueTarget.getNodePath()) < 1): self.state = Hooded.STATE_ATTACK