DirectSelection.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. from PandaObject import *
  2. from DirectGlobals import *
  3. from DirectUtil import *
  4. from DirectGeometry import *
  5. from DirectSelection import *
  6. COA_ORIGIN = 0
  7. COA_CENTER = 1
  8. # MRM: To do: handle broken node paths in selected and deselected dicts
  9. class DirectNodePath(NodePath):
  10. # A node path augmented with info, bounding box, and utility methods
  11. def __init__(self, nodePath):
  12. # Initialize the superclass
  13. NodePath.__init__(self)
  14. self.assign(nodePath)
  15. # Create a bounding box
  16. self.bbox = DirectBoundingBox(self)
  17. center = self.bbox.getCenter()
  18. # Create matrix to hold the offset between the nodepath
  19. # and its center of action (COA)
  20. self.mCoa2Dnp = Mat4(Mat4.identMat())
  21. if direct.coaMode == COA_CENTER:
  22. self.mCoa2Dnp.setRow(3, Vec4(center[0], center[1], center[2], 1))
  23. # Transform from nodePath to widget
  24. self.tDnp2Widget = TransformState.makeIdentity()
  25. def highlight(self):
  26. self.bbox.show()
  27. def dehighlight(self):
  28. self.bbox.hide()
  29. def getCenter(self):
  30. return self.bbox.getCenter()
  31. def getRadius(self):
  32. return self.bbox.getRadius()
  33. def getMin(self):
  34. return self.bbox.getMin()
  35. def getMax(self):
  36. return self.bbox.getMax()
  37. class SelectedNodePaths(PandaObject):
  38. def __init__(self):
  39. self.reset()
  40. def reset(self):
  41. self.selectedDict = {}
  42. self.deselectedDict = {}
  43. __builtins__["last"] = self.last = None
  44. def select(self, nodePath, fMultiSelect = 0):
  45. """ Select the specified node path. Multiselect as required """
  46. # Do nothing if nothing selected
  47. if not nodePath:
  48. print 'Nothing selected!!'
  49. return None
  50. # Reset selected objects and highlight if multiSelect is false
  51. if not fMultiSelect:
  52. self.deselectAll()
  53. # Get this pointer
  54. id = nodePath.id()
  55. # First see if its already in the selected dictionary
  56. dnp = self.getSelectedDict(id)
  57. # If so, we're done
  58. if not dnp:
  59. # See if it is in the deselected dictionary
  60. dnp = self.getDeselectedDict(id)
  61. if dnp:
  62. # Remove it from the deselected dictionary
  63. del(self.deselectedDict[id])
  64. # Show its bounding box
  65. dnp.highlight()
  66. else:
  67. # Didn't find it, create a new selectedNodePath instance
  68. dnp = DirectNodePath(nodePath)
  69. # Show its bounding box
  70. dnp.highlight()
  71. # Add it to the selected dictionary
  72. self.selectedDict[dnp.id()] = dnp
  73. # And update last
  74. __builtins__["last"] = self.last = dnp
  75. # Update cluster servers if this is a cluster client
  76. if direct.clusterMode == 'client':
  77. cluster.selectNodePath(dnp)
  78. return dnp
  79. def deselect(self, nodePath):
  80. """ Deselect the specified node path """
  81. # Get this pointer
  82. id = nodePath.id()
  83. # See if it is in the selected dictionary
  84. dnp = self.getSelectedDict(id)
  85. if dnp:
  86. # It was selected:
  87. # Hide its bounding box
  88. dnp.dehighlight()
  89. # Remove it from the selected dictionary
  90. del(self.selectedDict[id])
  91. # And keep track of it in the deselected dictionary
  92. self.deselectedDict[id] = dnp
  93. # Send a message
  94. messenger.send('DIRECT_deselectedNodePath', [dnp])
  95. # Update cluster servers if this is a cluster client
  96. if direct.clusterMode == 'client':
  97. cluster.deselectNodePath(dnp)
  98. return dnp
  99. def getSelectedAsList(self):
  100. """
  101. Return a list of all selected node paths. No verification of
  102. connectivity is performed on the members of the list
  103. """
  104. return self.selectedDict.values()[:]
  105. def __getitem__(self,index):
  106. return self.getSelectedAsList()[index]
  107. def getSelectedDict(self, id):
  108. """
  109. Search selectedDict for node path, try to repair broken node paths.
  110. """
  111. dnp = self.selectedDict.get(id, None)
  112. if dnp:
  113. return dnp
  114. else:
  115. # Not in selected dictionary
  116. return None
  117. def getDeselectedAsList(self):
  118. return self.deselectedDict.values()[:]
  119. def getDeselectedDict(self, id):
  120. """
  121. Search deselectedDict for node path, try to repair broken node paths.
  122. """
  123. dnp = self.deselectedDict.get(id, None)
  124. if dnp:
  125. # Yes
  126. return dnp
  127. else:
  128. # Not in deselected dictionary
  129. return None
  130. def forEachSelectedNodePathDo(self, func):
  131. """
  132. Perform given func on selected node paths. No node path
  133. connectivity verification performed
  134. """
  135. selectedNodePaths = self.getSelectedAsList()
  136. for nodePath in selectedNodePaths:
  137. func(nodePath)
  138. def forEachDeselectedNodePathDo(self, func):
  139. """
  140. Perform given func on deselected node paths. No node path
  141. connectivity verification performed
  142. """
  143. deselectedNodePaths = self.getDeselectedAsList()
  144. for nodePath in deselectedNodePaths:
  145. func(nodePath)
  146. def getWrtAll(self):
  147. self.forEachSelectedNodePathDo(self.getWrt)
  148. def getWrt(self, nodePath):
  149. nodePath.tDnp2Widget = nodePath.getTransform(direct.widget)
  150. def moveWrtWidgetAll(self):
  151. self.forEachSelectedNodePathDo(self.moveWrtWidget)
  152. def moveWrtWidget(self, nodePath):
  153. nodePath.setTransform(direct.widget, nodePath.tDnp2Widget)
  154. def deselectAll(self):
  155. self.forEachSelectedNodePathDo(self.deselect)
  156. def highlightAll(self):
  157. self.forEachSelectedNodePathDo(DirectNodePath.highlight)
  158. def dehighlightAll(self):
  159. self.forEachSelectedNodePathDo(DirectNodePath.dehighlight)
  160. def removeSelected(self):
  161. selected = self.last
  162. if selected:
  163. selected.remove()
  164. __builtins__["last"] = self.last = None
  165. def removeAll(self):
  166. # Remove all selected nodePaths from the Scene Graph
  167. self.forEachSelectedNodePathDo(NodePath.remove)
  168. def toggleVisSelected(self):
  169. selected = self.last
  170. # Toggle visibility of selected node paths
  171. if selected:
  172. selected.toggleVis()
  173. def toggleVisAll(self):
  174. # Toggle viz for all selected node paths
  175. self.forEachSelectedNodePathDo(NodePath.toggleVis)
  176. def isolateSelected(self):
  177. selected = self.last
  178. if selected:
  179. selected.isolate()
  180. def getDirectNodePath(self, nodePath):
  181. # Get this pointer
  182. id = nodePath.id()
  183. # First check selected dict
  184. dnp = self.getSelectedDict(id)
  185. if dnp:
  186. return dnp
  187. # Otherwise return result of deselected search
  188. return self.getDeselectedDict(id)
  189. def getNumSelected(self):
  190. return len(self.selectedDict.keys())
  191. class DirectBoundingBox:
  192. def __init__(self, nodePath):
  193. # Record the node path
  194. self.nodePath = nodePath
  195. # Compute bounds, min, max, etc.
  196. self.computeTightBounds()
  197. # Generate the bounding box
  198. self.lines = self.createBBoxLines()
  199. def computeTightBounds(self):
  200. # Compute bounding box using tighter calcTightBounds function
  201. # Need to clear out existing transform on node path
  202. tMat = Mat4()
  203. tMat.assign(self.nodePath.getMat())
  204. self.nodePath.clearMat()
  205. # Get bounds
  206. self.min = Point3(0)
  207. self.max = Point3(0)
  208. self.nodePath.calcTightBounds(self.min,self.max)
  209. # Calc center and radius
  210. self.center = Point3((self.min + self.max)/2.0)
  211. self.radius = Vec3(self.max - self.min).length()
  212. # Restore transform
  213. self.nodePath.setMat(tMat)
  214. del tMat
  215. def computeBounds(self):
  216. self.bounds = self.getBounds()
  217. if self.bounds.isEmpty() or self.bounds.isInfinite():
  218. self.center = Point3(0)
  219. self.radius = 1.0
  220. else:
  221. self.center = self.bounds.getCenter()
  222. self.radius = self.bounds.getRadius()
  223. self.min = Point3(self.center - Point3(self.radius))
  224. self.max = Point3(self.center + Point3(self.radius))
  225. def createBBoxLines(self):
  226. # Create a line segments object for the bbox
  227. lines = LineNodePath(hidden)
  228. lines.node().setName('bboxLines')
  229. lines.setColor( VBase4( 1., 0., 0., 1. ) )
  230. lines.setThickness( 0.5 )
  231. minX = self.min[0]
  232. minY = self.min[1]
  233. minZ = self.min[2]
  234. maxX = self.max[0]
  235. maxY = self.max[1]
  236. maxZ = self.max[2]
  237. # Bottom face
  238. lines.moveTo( minX, minY, minZ )
  239. lines.drawTo( maxX, minY, minZ )
  240. lines.drawTo( maxX, maxY, minZ )
  241. lines.drawTo( minX, maxY, minZ )
  242. lines.drawTo( minX, minY, minZ )
  243. # Front Edge/Top face
  244. lines.drawTo( minX, minY, maxZ )
  245. lines.drawTo( maxX, minY, maxZ )
  246. lines.drawTo( maxX, maxY, maxZ )
  247. lines.drawTo( minX, maxY, maxZ )
  248. lines.drawTo( minX, minY, maxZ )
  249. # Three remaining edges
  250. lines.moveTo( maxX, minY, minZ )
  251. lines.drawTo( maxX, minY, maxZ )
  252. lines.moveTo( maxX, maxY, minZ )
  253. lines.drawTo( maxX, maxY, maxZ )
  254. lines.moveTo( minX, maxY, minZ )
  255. lines.drawTo( minX, maxY, maxZ )
  256. # Create and return bbox lines
  257. lines.create()
  258. # Make sure bbox is never lit or drawn in wireframe
  259. useDirectRenderStyle(lines)
  260. return lines
  261. def updateBBoxLines(self):
  262. ls = self.lines.lineSegs
  263. minX = self.min[0]
  264. minY = self.min[1]
  265. minZ = self.min[2]
  266. maxX = self.max[0]
  267. maxY = self.max[1]
  268. maxZ = self.max[2]
  269. # Bottom face
  270. ls.setVertex( 0, minX, minY, minZ )
  271. ls.setVertex( 1, maxX, minY, minZ )
  272. ls.setVertex( 2, maxX, maxY, minZ )
  273. ls.setVertex( 3, minX, maxY, minZ )
  274. ls.setVertex( 4, minX, minY, minZ )
  275. # Front Edge/Top face
  276. ls.setVertex( 5, minX, minY, maxZ )
  277. ls.setVertex( 6, maxX, minY, maxZ )
  278. ls.setVertex( 7, maxX, maxY, maxZ )
  279. ls.setVertex( 8, minX, maxY, maxZ )
  280. ls.setVertex( 9, minX, minY, maxZ )
  281. # Three remaining edges
  282. ls.setVertex( 10, maxX, minY, minZ )
  283. ls.setVertex( 11, maxX, minY, maxZ )
  284. ls.setVertex( 12, maxX, maxY, minZ )
  285. ls.setVertex( 13, maxX, maxY, maxZ )
  286. ls.setVertex( 14, minX, maxY, minZ )
  287. ls.setVertex( 15, minX, maxY, maxZ )
  288. def getBounds(self):
  289. # Get a node path's bounds
  290. nodeBounds = BoundingSphere()
  291. nodeBounds.extendBy(self.nodePath.node().getInternalBound())
  292. for child in self.nodePath.getChildrenAsList():
  293. nodeBounds.extendBy(child.getBounds())
  294. return nodeBounds.makeCopy()
  295. def show(self):
  296. self.lines.reparentTo(self.nodePath)
  297. def hide(self):
  298. self.lines.reparentTo(hidden)
  299. def getCenter(self):
  300. return self.center
  301. def getRadius(self):
  302. return self.radius
  303. def getMin(self):
  304. return self.min
  305. def getMax(self):
  306. return self.max
  307. def vecAsString(self, vec):
  308. return '%.2f %.2f %.2f' % (vec[0], vec[1], vec[2])
  309. def __repr__(self):
  310. return (`self.__class__` +
  311. '\nNodePath:\t%s\n' % self.nodePath.getName() +
  312. 'Min:\t\t%s\n' % self.vecAsString(self.min) +
  313. 'Max:\t\t%s\n' % self.vecAsString(self.max) +
  314. 'Center:\t\t%s\n' % self.vecAsString(self.center) +
  315. 'Radius:\t\t%.2f' % self.radius
  316. )
  317. class SelectionQueue(CollisionHandlerQueue):
  318. def __init__(self, fromNP = render):
  319. # Initialize the superclass
  320. CollisionHandlerQueue.__init__(self)
  321. # Current index and entry in collision queue
  322. self.index = -1
  323. self.entry = None
  324. self.skipFlags = SKIP_NONE
  325. # Create a collision node path attached to the given NP
  326. self.collisionNodePath = NodePath(CollisionNode("collisionNP"))
  327. self.setFromNP(fromNP)
  328. # Don't pay the penalty of drawing this collision ray
  329. self.collisionNodePath.hide()
  330. self.collisionNode = self.collisionNodePath.node()
  331. # Intersect with geometry to begin with
  332. self.collideWithGeom()
  333. # And a traverser to do the actual collision tests
  334. self.ct = CollisionTraverser()
  335. # Let the traverser know about the collision node and the queue
  336. self.ct.addCollider(self.collisionNode, self)
  337. # List of objects that can't be selected
  338. self.unpickable = UNPICKABLE
  339. # Derived class must add Collider to complete initialization
  340. def setFromNP(self, fromNP):
  341. # Update fromNP
  342. self.collisionNodePath.reparentTo(fromNP)
  343. def addCollider(self, collider):
  344. # Inherited class must call this function to specify collider object
  345. # Record collision object
  346. self.collider = collider
  347. # Add the collider to the collision Node
  348. self.collisionNode.addSolid( self.collider )
  349. def collideWithGeom(self):
  350. self.collisionNode.setIntoCollideMask(BitMask32().allOff())
  351. self.collisionNode.setFromCollideMask(BitMask32().allOff())
  352. self.collisionNode.setCollideGeom(1)
  353. def collideWithWidget(self):
  354. self.collisionNode.setIntoCollideMask(BitMask32().allOff())
  355. mask = BitMask32()
  356. mask.setWord(0x80000000)
  357. self.collisionNode.setFromCollideMask(mask)
  358. self.collisionNode.setCollideGeom(0)
  359. def addUnpickable(self, item):
  360. if item not in self.unpickable:
  361. self.unpickable.append(item)
  362. def removeUnpickable(self, item):
  363. if item in self.unpickable:
  364. self.unpickable.remove(item)
  365. def setCurrentIndex(self, index):
  366. if (index < 0) or (index >= self.getNumEntries()):
  367. self.index = -1
  368. else:
  369. self.index = index
  370. def setCurrentEntry(self, entry):
  371. self.entry = entry
  372. def getCurrentEntry(self):
  373. return self.entry
  374. def isEntryBackfacing(self, entry):
  375. # If dot product of collision point surface normal and
  376. # ray from camera to collision point is positive, we are
  377. # looking at the backface of the polygon
  378. v = Vec3(entry.getFromIntersectionPoint())
  379. v.normalize()
  380. return v.dot(entry.getFromSurfaceNormal()) >= 0
  381. def findCollisionEntry(self, skipFlags = SKIP_NONE, startIndex = 0 ):
  382. # Init self.index and self.entry
  383. self.setCurrentIndex(-1)
  384. self.setCurrentEntry(None)
  385. # Pick out the closest object that isn't a widget
  386. for i in range(startIndex,self.getNumEntries()):
  387. entry = self.getEntry(i)
  388. nodePath = entry.getIntoNodePath()
  389. if (skipFlags & SKIP_HIDDEN) and nodePath.isHidden():
  390. # Skip if hidden node
  391. pass
  392. elif (skipFlags & SKIP_BACKFACE) and self.isEntryBackfacing(entry):
  393. # Skip, if backfacing poly
  394. pass
  395. elif ((skipFlags & SKIP_CAMERA) and
  396. (camera in nodePath.getAncestry())):
  397. # Skip if parented to a camera.
  398. pass
  399. # Can pick unpickable, use the first visible node
  400. elif ((skipFlags & SKIP_UNPICKABLE) and
  401. (nodePath.getName() in self.unpickable)):
  402. # Skip if in unpickable list
  403. pass
  404. else:
  405. self.setCurrentIndex(i)
  406. self.setCurrentEntry(entry)
  407. break
  408. return self.getCurrentEntry()
  409. class SelectionRay(SelectionQueue):
  410. def __init__(self, fromNP = render):
  411. # Initialize the superclass
  412. SelectionQueue.__init__(self, fromNP)
  413. self.addCollider(CollisionRay())
  414. def pick(self, targetNodePath):
  415. # Determine ray direction based upon the mouse coordinates
  416. if direct:
  417. mx = direct.dr.mouseX
  418. my = direct.dr.mouseY
  419. else:
  420. mx = base.mouseWatcherNode.getMouseX()
  421. my = base.mouseWatcherNode.getMouseY()
  422. self.collider.setFromLens( base.camNode, mx, my )
  423. self.ct.traverse( targetNodePath )
  424. self.sortEntries()
  425. def pickGeom(self, targetNodePath = render, skipFlags = SKIP_ALL ):
  426. self.collideWithGeom()
  427. self.pick(targetNodePath)
  428. # Determine collision entry
  429. return self.findCollisionEntry(skipFlags)
  430. def pickWidget(self, targetNodePath = render, skipFlags = SKIP_NONE ):
  431. self.collideWithWidget()
  432. self.pick(targetNodePath)
  433. # Determine collision entry
  434. return self.findCollisionEntry(skipFlags)
  435. def pick3D(self, targetNodePath, origin, dir):
  436. # Determine ray direction based upon the mouse coordinates
  437. self.collider.setOrigin( origin )
  438. self.collider.setDirection( dir )
  439. self.ct.traverse( targetNodePath )
  440. self.sortEntries()
  441. def pickGeom3D(self, targetNodePath = render,
  442. origin = Point3(0), dir = Vec3(0,0,-1),
  443. skipFlags = SKIP_HIDDEN | SKIP_CAMERA ):
  444. self.collideWithGeom()
  445. self.pick3D(targetNodePath, origin, dir)
  446. # Determine collision entry
  447. return self.findCollisionEntry(skipFlags)
  448. class SelectionSegment(SelectionQueue):
  449. # Like a selection ray but with two endpoints instead of an endpoint
  450. # and a direction
  451. def __init__(self, fromNP = render, numSegments = 1):
  452. # Initialize the superclass
  453. SelectionQueue.__init__(self, fromNP)
  454. self.colliders = []
  455. self.numColliders = 0
  456. for i in range(numSegments):
  457. self.addCollider(CollisionSegment())
  458. def addCollider(self, collider):
  459. # Record new collision object
  460. self.colliders.append(collider)
  461. # Add the collider to the collision Node
  462. self.collisionNode.addSolid( collider )
  463. self.numColliders += 1
  464. def pickGeom(self, targetNodePath = render, endPointList = [],
  465. skipFlags = SKIP_HIDDEN | SKIP_CAMERA ):
  466. self.collideWithGeom()
  467. for i in range(min(len(endPointList), self.numColliders)):
  468. pointA, pointB = endPointList[i]
  469. collider = self.colliders[i]
  470. collider.setPointA( pointA )
  471. collider.setPointB( pointB )
  472. self.ct.traverse( targetNodePath )
  473. # self.sortEntries()
  474. # Determine collision entry
  475. return self.findCollisionEntry(skipFlags)