DirectSelection.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796
  1. from direct.showbase.DirectObject import DirectObject
  2. from .DirectGlobals import *
  3. from .DirectUtil import *
  4. from .DirectGeometry import *
  5. COA_ORIGIN = 0
  6. COA_CENTER = 1
  7. # MRM: To do: handle broken node paths in selected and deselected dicts
  8. class DirectNodePath(NodePath):
  9. # A node path augmented with info, bounding box, and utility methods
  10. def __init__(self, nodePath, bboxColor=None):
  11. # Initialize the superclass
  12. NodePath.__init__(self)
  13. self.assign(nodePath)
  14. # Create a bounding box
  15. self.bbox = DirectBoundingBox(self, bboxColor)
  16. center = self.bbox.getCenter()
  17. # Create matrix to hold the offset between the nodepath
  18. # and its center of action (COA)
  19. self.mCoa2Dnp = Mat4(Mat4.identMat())
  20. if base.direct.coaMode == COA_CENTER:
  21. self.mCoa2Dnp.setRow(3, Vec4(center[0], center[1], center[2], 1))
  22. # Transform from nodePath to widget
  23. self.tDnp2Widget = TransformState.makeIdentity()
  24. def highlight(self, fRecompute = 1):
  25. if fRecompute:
  26. pass
  27. #self.bbox.recompute()
  28. self.bbox.show()
  29. def dehighlight(self):
  30. self.bbox.hide()
  31. def getCenter(self):
  32. return self.bbox.getCenter()
  33. def getRadius(self):
  34. return self.bbox.getRadius()
  35. def getMin(self):
  36. return self.bbox.getMin()
  37. def getMax(self):
  38. return self.bbox.getMax()
  39. class SelectedNodePaths(DirectObject):
  40. def __init__(self):
  41. self.reset()
  42. self.tagList = []
  43. def addTag(self, tag):
  44. if tag not in self.tagList:
  45. self.tagList.append(tag)
  46. def removeTag(self, tag):
  47. self.tagList.remove(tag)
  48. def reset(self):
  49. self.selectedDict = {}
  50. self.selectedList = [] # [gjeon] to maintain selected order
  51. self.deselectedDict = {}
  52. __builtins__["last"] = self.last = None
  53. def select(self, nodePath, fMultiSelect = 0, fSelectTag = 1):
  54. """ Select the specified node path. Multiselect as required """
  55. # Do nothing if nothing selected
  56. if not nodePath:
  57. print('Nothing selected!!')
  58. return None
  59. # Reset selected objects and highlight if multiSelect is false
  60. if not fMultiSelect:
  61. self.deselectAll()
  62. # Select tagged object if present
  63. if fSelectTag:
  64. for tag in self.tagList:
  65. if nodePath.hasNetTag(tag):
  66. nodePath = nodePath.findNetTag(tag)
  67. break
  68. # Get this pointer
  69. id = nodePath.get_key()
  70. # First see if its already in the selected dictionary
  71. dnp = self.getSelectedDict(id)
  72. # If so, deselect it
  73. if dnp:
  74. self.deselect(nodePath)
  75. return None
  76. else:
  77. # See if it is in the deselected dictionary
  78. dnp = self.getDeselectedDict(id)
  79. if dnp:
  80. # Remove it from the deselected dictionary
  81. del(self.deselectedDict[id])
  82. # Show its bounding box
  83. dnp.highlight()
  84. else:
  85. # Didn't find it, create a new selectedNodePath instance
  86. dnp = DirectNodePath(nodePath)
  87. # Show its bounding box
  88. dnp.highlight(fRecompute = 0)
  89. # Add it to the selected dictionary
  90. self.selectedDict[dnp.get_key()] = dnp
  91. self.selectedList.append(dnp) # [gjeon]
  92. # And update last
  93. __builtins__["last"] = self.last = dnp
  94. # Update cluster servers if this is a cluster client
  95. if base.direct.clusterMode == 'client':
  96. cluster.selectNodePath(dnp)
  97. return dnp
  98. def deselect(self, nodePath):
  99. """ Deselect the specified node path """
  100. # Get this pointer
  101. id = nodePath.get_key()
  102. # See if it is in the selected dictionary
  103. dnp = self.getSelectedDict(id)
  104. if dnp:
  105. # It was selected:
  106. # Hide its bounding box
  107. dnp.dehighlight()
  108. # Remove it from the selected dictionary
  109. del(self.selectedDict[id])
  110. if dnp in self.selectedList: # [gjeon]
  111. self.selectedList.remove(dnp)
  112. # And keep track of it in the deselected dictionary
  113. self.deselectedDict[id] = dnp
  114. # Send a message
  115. messenger.send('DIRECT_deselectedNodePath', [dnp])
  116. # Update cluster servers if this is a cluster client
  117. if base.direct.clusterMode == 'client':
  118. cluster.deselectNodePath(dnp)
  119. return dnp
  120. def getSelectedAsList(self):
  121. """
  122. Return a list of all selected node paths. No verification of
  123. connectivity is performed on the members of the list
  124. """
  125. #return self.selectedDict.values()[:]
  126. return self.selectedList[:] # [gjeon] now return the list with selected order
  127. def __getitem__(self, index):
  128. return self.getSelectedAsList()[index]
  129. def getSelectedDict(self, id):
  130. """
  131. Search selectedDict for node path, try to repair broken node paths.
  132. """
  133. dnp = self.selectedDict.get(id, None)
  134. if dnp:
  135. return dnp
  136. else:
  137. # Not in selected dictionary
  138. return None
  139. def getDeselectedAsList(self):
  140. return list(self.deselectedDict.values())
  141. def getDeselectedDict(self, id):
  142. """
  143. Search deselectedDict for node path, try to repair broken node paths.
  144. """
  145. dnp = self.deselectedDict.get(id, None)
  146. if dnp:
  147. # Yes
  148. return dnp
  149. else:
  150. # Not in deselected dictionary
  151. return None
  152. def forEachSelectedNodePathDo(self, func):
  153. """
  154. Perform given func on selected node paths. No node path
  155. connectivity verification performed
  156. """
  157. selectedNodePaths = self.getSelectedAsList()
  158. for nodePath in selectedNodePaths:
  159. func(nodePath)
  160. def forEachDeselectedNodePathDo(self, func):
  161. """
  162. Perform given func on deselected node paths. No node path
  163. connectivity verification performed
  164. """
  165. deselectedNodePaths = self.getDeselectedAsList()
  166. for nodePath in deselectedNodePaths:
  167. func(nodePath)
  168. def getWrtAll(self):
  169. self.forEachSelectedNodePathDo(self.getWrt)
  170. def getWrt(self, nodePath):
  171. nodePath.tDnp2Widget = nodePath.getTransform(base.direct.widget)
  172. def moveWrtWidgetAll(self):
  173. self.forEachSelectedNodePathDo(self.moveWrtWidget)
  174. def moveWrtWidget(self, nodePath):
  175. nodePath.setTransform(base.direct.widget, nodePath.tDnp2Widget)
  176. def deselectAll(self):
  177. self.forEachSelectedNodePathDo(self.deselect)
  178. def highlightAll(self):
  179. self.forEachSelectedNodePathDo(DirectNodePath.highlight)
  180. def dehighlightAll(self):
  181. self.forEachSelectedNodePathDo(DirectNodePath.dehighlight)
  182. def removeSelected(self):
  183. selected = self.last
  184. if selected:
  185. selected.remove()
  186. __builtins__["last"] = self.last = None
  187. def removeAll(self):
  188. # Remove all selected nodePaths from the Scene Graph
  189. self.forEachSelectedNodePathDo(NodePath.remove)
  190. def toggleVisSelected(self):
  191. selected = self.last
  192. # Toggle visibility of selected node paths
  193. if selected:
  194. if selected.isHidden():
  195. selected.show()
  196. else:
  197. selected.hide()
  198. def toggleVisAll(self):
  199. # Toggle viz for all selected node paths
  200. selectedNodePaths = self.getSelectedAsList()
  201. for nodePath in selectedNodePaths:
  202. if nodePath.isHidden():
  203. nodePath.show()
  204. else:
  205. nodePath.hide()
  206. def isolateSelected(self):
  207. selected = self.last
  208. if selected:
  209. selected.showAllDescendents()
  210. for sib in selected.getParent().getChildren():
  211. if sib.node() != selected.node():
  212. sib.hide()
  213. def getDirectNodePath(self, nodePath):
  214. # Get this pointer
  215. id = nodePath.get_key()
  216. # First check selected dict
  217. dnp = self.getSelectedDict(id)
  218. if dnp:
  219. return dnp
  220. # Otherwise return result of deselected search
  221. return self.getDeselectedDict(id)
  222. def getNumSelected(self):
  223. return len(self.selectedDict)
  224. class DirectBoundingBox:
  225. def __init__(self, nodePath, bboxColor=None):
  226. # Record the node path
  227. self.nodePath = nodePath
  228. # Compute bounds, min, max, etc.
  229. self.computeTightBounds()
  230. # Generate the bounding box
  231. self.lines = self.createBBoxLines(bboxColor)
  232. def recompute(self):
  233. # Compute bounds, min, max, etc.
  234. self.computeTightBounds()
  235. self.updateBBoxLines()
  236. def computeTightBounds(self):
  237. # Compute bounding box using tighter calcTightBounds function
  238. # Need to clear out existing transform on node path
  239. tMat = Mat4(self.nodePath.getMat())
  240. self.nodePath.clearMat()
  241. # Get bounds
  242. self.min = Point3(0)
  243. self.max = Point3(0)
  244. self.nodePath.calcTightBounds(self.min, self.max)
  245. # Calc center and radius
  246. self.center = Point3((self.min + self.max)/2.0)
  247. self.radius = Vec3(self.max - self.min).length()
  248. # Restore transform
  249. self.nodePath.setMat(tMat)
  250. del tMat
  251. def computeBounds(self):
  252. self.bounds = self.getBounds()
  253. if self.bounds.isEmpty() or self.bounds.isInfinite():
  254. self.center = Point3(0)
  255. self.radius = 1.0
  256. else:
  257. self.center = self.bounds.getCenter()
  258. self.radius = self.bounds.getRadius()
  259. self.min = Point3(self.center - Point3(self.radius))
  260. self.max = Point3(self.center + Point3(self.radius))
  261. def createBBoxLines(self, bboxColor=None):
  262. # Create a line segments object for the bbox
  263. lines = LineNodePath(hidden)
  264. lines.node().setName('bboxLines')
  265. if (bboxColor):
  266. lines.setColor(VBase4(*bboxColor))
  267. else:
  268. lines.setColor(VBase4(1., 0., 0., 1.))
  269. lines.setThickness(0.5)
  270. minX = self.min[0]
  271. minY = self.min[1]
  272. minZ = self.min[2]
  273. maxX = self.max[0]
  274. maxY = self.max[1]
  275. maxZ = self.max[2]
  276. # Bottom face
  277. lines.moveTo(minX, minY, minZ)
  278. lines.drawTo(maxX, minY, minZ)
  279. lines.drawTo(maxX, maxY, minZ)
  280. lines.drawTo(minX, maxY, minZ)
  281. lines.drawTo(minX, minY, minZ)
  282. # Front Edge/Top face
  283. lines.drawTo(minX, minY, maxZ)
  284. lines.drawTo(maxX, minY, maxZ)
  285. lines.drawTo(maxX, maxY, maxZ)
  286. lines.drawTo(minX, maxY, maxZ)
  287. lines.drawTo(minX, minY, maxZ)
  288. # Three remaining edges
  289. lines.moveTo(maxX, minY, minZ)
  290. lines.drawTo(maxX, minY, maxZ)
  291. lines.moveTo(maxX, maxY, minZ)
  292. lines.drawTo(maxX, maxY, maxZ)
  293. lines.moveTo(minX, maxY, minZ)
  294. lines.drawTo(minX, maxY, maxZ)
  295. # Create and return bbox lines
  296. lines.create()
  297. # Make sure bbox is never lit or drawn in wireframe
  298. useDirectRenderStyle(lines)
  299. return lines
  300. def setBoxColorScale(self, r, g, b, a):
  301. if (self.lines):
  302. self.lines.reset()
  303. self.lines = None
  304. self.lines = self.createBBoxLines((r, g, b, a))
  305. self.show()
  306. def updateBBoxLines(self):
  307. ls = self.lines.lineSegs
  308. minX = self.min[0]
  309. minY = self.min[1]
  310. minZ = self.min[2]
  311. maxX = self.max[0]
  312. maxY = self.max[1]
  313. maxZ = self.max[2]
  314. # Bottom face
  315. ls.setVertex(0, minX, minY, minZ)
  316. ls.setVertex(1, maxX, minY, minZ)
  317. ls.setVertex(2, maxX, maxY, minZ)
  318. ls.setVertex(3, minX, maxY, minZ)
  319. ls.setVertex(4, minX, minY, minZ)
  320. # Front Edge/Top face
  321. ls.setVertex(5, minX, minY, maxZ)
  322. ls.setVertex(6, maxX, minY, maxZ)
  323. ls.setVertex(7, maxX, maxY, maxZ)
  324. ls.setVertex(8, minX, maxY, maxZ)
  325. ls.setVertex(9, minX, minY, maxZ)
  326. # Three remaining edges
  327. ls.setVertex(10, maxX, minY, minZ)
  328. ls.setVertex(11, maxX, minY, maxZ)
  329. ls.setVertex(12, maxX, maxY, minZ)
  330. ls.setVertex(13, maxX, maxY, maxZ)
  331. ls.setVertex(14, minX, maxY, minZ)
  332. ls.setVertex(15, minX, maxY, maxZ)
  333. def getBounds(self):
  334. # Get a node path's bounds
  335. nodeBounds = BoundingSphere()
  336. nodeBounds.extendBy(self.nodePath.node().getInternalBound())
  337. for child in self.nodePath.getChildren():
  338. nodeBounds.extendBy(child.getBounds())
  339. return nodeBounds.makeCopy()
  340. def show(self):
  341. self.lines.reparentTo(self.nodePath)
  342. def hide(self):
  343. self.lines.reparentTo(hidden)
  344. def getCenter(self):
  345. return self.center
  346. def getRadius(self):
  347. return self.radius
  348. def getMin(self):
  349. return self.min
  350. def getMax(self):
  351. return self.max
  352. def vecAsString(self, vec):
  353. return '%.2f %.2f %.2f' % (vec[0], vec[1], vec[2])
  354. def __repr__(self):
  355. return (repr(self.__class__) +
  356. '\nNodePath:\t%s\n' % self.nodePath.getName() +
  357. 'Min:\t\t%s\n' % self.vecAsString(self.min) +
  358. 'Max:\t\t%s\n' % self.vecAsString(self.max) +
  359. 'Center:\t\t%s\n' % self.vecAsString(self.center) +
  360. 'Radius:\t\t%.2f' % self.radius
  361. )
  362. class SelectionQueue(CollisionHandlerQueue):
  363. def __init__(self, parentNP = None):
  364. if parentNP is None:
  365. parentNP = render
  366. # Initialize the superclass
  367. CollisionHandlerQueue.__init__(self)
  368. # Current index and entry in collision queue
  369. self.index = -1
  370. self.entry = None
  371. self.skipFlags = SKIP_NONE
  372. # Create a collision node path attached to the given NP
  373. self.collisionNodePath = NodePath(CollisionNode("collisionNP"))
  374. self.setParentNP(parentNP)
  375. # Don't pay the penalty of drawing this collision ray
  376. self.collisionNodePath.hide()
  377. self.collisionNode = self.collisionNodePath.node()
  378. # Intersect with geometry to begin with
  379. self.collideWithGeom()
  380. # And a traverser to do the actual collision tests
  381. self.ct = CollisionTraverser("DirectSelection")
  382. self.ct.setRespectPrevTransform(False)
  383. # Let the traverser know about the collision node and the queue
  384. self.ct.addCollider(self.collisionNodePath, self)
  385. # List of objects that can't be selected
  386. self.unpickable = UNPICKABLE
  387. # Derived class must add Collider to complete initialization
  388. def setParentNP(self, parentNP):
  389. # Update collisionNodePath's parent
  390. self.collisionNodePath.reparentTo(parentNP)
  391. def addCollider(self, collider):
  392. # Inherited class must call this function to specify collider object
  393. # Record collision object
  394. self.collider = collider
  395. # Add the collider to the collision Node
  396. self.collisionNode.addSolid(self.collider)
  397. def collideWithBitMask(self, bitMask):
  398. # The into collide mask is the bit pattern colliders look at
  399. # when deciding whether or not to test for a collision "into"
  400. # this collision solid. Set to all Off so this collision solid
  401. # will not be considered in any collision tests
  402. self.collisionNode.setIntoCollideMask(BitMask32().allOff())
  403. # The from collide mask is the bit pattern *this* collision solid
  404. # compares against the into collide mask of candidate collision solids
  405. # Turn this mask all off since we're not testing for collisions against
  406. # collision solids
  407. self.collisionNode.setFromCollideMask(bitMask)
  408. def collideWithGeom(self):
  409. # The into collide mask is the bit pattern colliders look at
  410. # when deciding whether or not to test for a collision "into"
  411. # this collision solid. Set to all Off so this collision solid
  412. # will not be considered in any collision tests
  413. self.collisionNode.setIntoCollideMask(BitMask32().allOff())
  414. # The from collide mask is the bit pattern *this* collision solid
  415. # compares against the into collide mask of candidate collision solids
  416. # Turn this mask all off since we're not testing for collisions against
  417. # collision solids
  418. self.collisionNode.setFromCollideMask(GeomNode.getDefaultCollideMask())
  419. def collideWithWidget(self):
  420. # This collision node should not be tested against by any other
  421. # collision solids
  422. self.collisionNode.setIntoCollideMask(BitMask32().allOff())
  423. # This collision node will test for collisions with any collision
  424. # solids with a bit mask set to 0x80000000
  425. mask = BitMask32()
  426. mask.setWord(0x80000000)
  427. self.collisionNode.setFromCollideMask(mask)
  428. def addUnpickable(self, item):
  429. if item not in self.unpickable:
  430. self.unpickable.append(item)
  431. def removeUnpickable(self, item):
  432. if item in self.unpickable:
  433. self.unpickable.remove(item)
  434. def setCurrentIndex(self, index):
  435. if (index < 0) or (index >= self.getNumEntries()):
  436. self.index = -1
  437. else:
  438. self.index = index
  439. def setCurrentEntry(self, entry):
  440. self.entry = entry
  441. def getCurrentEntry(self):
  442. return self.entry
  443. def isEntryBackfacing(self, entry):
  444. # If dot product of collision point surface normal and
  445. # ray from camera to collision point is positive, we are
  446. # looking at the backface of the polygon
  447. if not entry.hasSurfaceNormal():
  448. # Well, no way to tell. Assume we're not backfacing.
  449. return 0
  450. if direct:
  451. cam = base.direct.cam
  452. else:
  453. cam = base.cam
  454. fromNodePath = entry.getFromNodePath()
  455. v = Vec3(entry.getSurfacePoint(fromNodePath))
  456. n = entry.getSurfaceNormal(fromNodePath)
  457. # Convert to camera space for backfacing test
  458. if self.collisionNodePath.getParent() != cam:
  459. # Problem: assumes base.cam is the camera in question
  460. p2cam = self.collisionNodePath.getParent().getMat(cam)
  461. v = Vec3(p2cam.xformPoint(v))
  462. n = p2cam.xformVec(n)
  463. # Normalize and check angle between to vectors
  464. v.normalize()
  465. return v.dot(n) >= 0
  466. def findNextCollisionEntry(self, skipFlags = SKIP_NONE):
  467. return self.findCollisionEntry(skipFlags, self.index + 1)
  468. def findCollisionEntry(self, skipFlags = SKIP_NONE, startIndex = 0):
  469. # Init self.index and self.entry
  470. self.setCurrentIndex(-1)
  471. self.setCurrentEntry(None)
  472. # Pick out the closest object that isn't a widget
  473. for i in range(startIndex, self.getNumEntries()):
  474. entry = self.getEntry(i)
  475. nodePath = entry.getIntoNodePath()
  476. if (skipFlags & SKIP_HIDDEN) and nodePath.isHidden():
  477. # Skip if hidden node
  478. pass
  479. elif (skipFlags & SKIP_BACKFACE) and self.isEntryBackfacing(entry):
  480. # Skip, if backfacing poly
  481. pass
  482. elif ((skipFlags & SKIP_CAMERA) and
  483. (camera in nodePath.getAncestors())):
  484. # Skip if parented to a camera.
  485. pass
  486. # Can pick unpickable, use the first visible node
  487. elif ((skipFlags & SKIP_UNPICKABLE) and
  488. (nodePath.getName() in self.unpickable)):
  489. # Skip if in unpickable list
  490. pass
  491. elif base.direct and\
  492. ((skipFlags & SKIP_WIDGET) and
  493. (nodePath.getTag('WidgetName') != base.direct.widget.getName())):
  494. # Skip if this widget part is not belong to current widget
  495. pass
  496. elif base.direct and\
  497. ((skipFlags & SKIP_WIDGET) and base.direct.fControl and
  498. (nodePath.getName()[2:] == 'ring')):
  499. # Skip when ununiformly scale in ortho view
  500. pass
  501. else:
  502. self.setCurrentIndex(i)
  503. self.setCurrentEntry(entry)
  504. break
  505. return self.getCurrentEntry()
  506. class SelectionRay(SelectionQueue):
  507. def __init__(self, parentNP = None):
  508. if parentNP is None:
  509. parentNP = render
  510. # Initialize the superclass
  511. SelectionQueue.__init__(self, parentNP)
  512. self.addCollider(CollisionRay())
  513. def pick(self, targetNodePath, xy = None):
  514. # Determine ray direction based upon the mouse coordinates
  515. if xy:
  516. mx = xy[0]
  517. my = xy[1]
  518. elif direct:
  519. mx = base.direct.dr.mouseX
  520. my = base.direct.dr.mouseY
  521. else:
  522. if not base.mouseWatcherNode.hasMouse():
  523. # No mouse in window.
  524. self.clearEntries()
  525. return
  526. mx = base.mouseWatcherNode.getMouseX()
  527. my = base.mouseWatcherNode.getMouseY()
  528. if direct:
  529. self.collider.setFromLens(base.direct.camNode, mx, my)
  530. else:
  531. self.collider.setFromLens(base.camNode, mx, my)
  532. self.ct.traverse(targetNodePath)
  533. self.sortEntries()
  534. def pickBitMask(self, bitMask = BitMask32.allOff(),
  535. targetNodePath = None,
  536. skipFlags = SKIP_ALL):
  537. if targetNodePath is None:
  538. targetNodePath = render
  539. self.collideWithBitMask(bitMask)
  540. self.pick(targetNodePath)
  541. # Determine collision entry
  542. return self.findCollisionEntry(skipFlags)
  543. def pickGeom(self, targetNodePath = None, skipFlags = SKIP_ALL,
  544. xy = None):
  545. if targetNodePath is None:
  546. targetNodePath = render
  547. self.collideWithGeom()
  548. self.pick(targetNodePath, xy = xy)
  549. # Determine collision entry
  550. return self.findCollisionEntry(skipFlags)
  551. def pickWidget(self, targetNodePath = None, skipFlags = SKIP_NONE):
  552. if targetNodePath is None:
  553. targetNodePath = render
  554. self.collideWithWidget()
  555. self.pick(targetNodePath)
  556. # Determine collision entry
  557. return self.findCollisionEntry(skipFlags)
  558. def pick3D(self, targetNodePath, origin, dir):
  559. # Determine ray direction based upon the mouse coordinates
  560. self.collider.setOrigin(origin)
  561. self.collider.setDirection(dir)
  562. self.ct.traverse(targetNodePath)
  563. self.sortEntries()
  564. def pickGeom3D(self, targetNodePath = None,
  565. origin = Point3(0), dir = Vec3(0, 0, -1),
  566. skipFlags = SKIP_HIDDEN | SKIP_CAMERA):
  567. if targetNodePath is None:
  568. targetNodePath = render
  569. self.collideWithGeom()
  570. self.pick3D(targetNodePath, origin, dir)
  571. # Determine collision entry
  572. return self.findCollisionEntry(skipFlags)
  573. def pickBitMask3D(self, bitMask = BitMask32.allOff(),
  574. targetNodePath = None,
  575. origin = Point3(0), dir = Vec3(0, 0, -1),
  576. skipFlags = SKIP_ALL):
  577. if targetNodePath is None:
  578. targetNodePath = render
  579. self.collideWithBitMask(bitMask)
  580. self.pick3D(targetNodePath, origin, dir)
  581. # Determine collision entry
  582. return self.findCollisionEntry(skipFlags)
  583. class SelectionSegment(SelectionQueue):
  584. # Like a selection ray but with two endpoints instead of an endpoint
  585. # and a direction
  586. def __init__(self, parentNP = None, numSegments = 1):
  587. if parentNP is None:
  588. parentNP = render
  589. # Initialize the superclass
  590. SelectionQueue.__init__(self, parentNP)
  591. self.colliders = []
  592. self.numColliders = 0
  593. for i in range(numSegments):
  594. self.addCollider(CollisionSegment())
  595. def addCollider(self, collider):
  596. # Record new collision object
  597. self.colliders.append(collider)
  598. # Add the collider to the collision Node
  599. self.collisionNode.addSolid(collider)
  600. self.numColliders += 1
  601. def pickGeom(self, targetNodePath = None, endPointList = [],
  602. skipFlags = SKIP_HIDDEN | SKIP_CAMERA):
  603. if targetNodePath is None:
  604. targetNodePath = render
  605. self.collideWithGeom()
  606. for i in range(min(len(endPointList), self.numColliders)):
  607. pointA, pointB = endPointList[i]
  608. collider = self.colliders[i]
  609. collider.setPointA(pointA)
  610. collider.setPointB(pointB)
  611. self.ct.traverse(targetNodePath)
  612. # Determine collision entry
  613. return self.findCollisionEntry(skipFlags)
  614. def pickBitMask(self, bitMask = BitMask32.allOff(),
  615. targetNodePath = None, endPointList = [],
  616. skipFlags = SKIP_HIDDEN | SKIP_CAMERA):
  617. if targetNodePath is None:
  618. targetNodePath = render
  619. self.collideWithBitMask(bitMask)
  620. for i in range(min(len(endPointList), self.numColliders)):
  621. pointA, pointB = endPointList[i]
  622. collider = self.colliders[i]
  623. collider.setPointA(pointA)
  624. collider.setPointB(pointB)
  625. self.ct.traverse(targetNodePath)
  626. # Determine collision entry
  627. return self.findCollisionEntry(skipFlags)
  628. class SelectionSphere(SelectionQueue):
  629. # Wrapper around collision sphere
  630. def __init__(self, parentNP = None, numSpheres = 1):
  631. if parentNP is None:
  632. parentNP = render
  633. # Initialize the superclass
  634. SelectionQueue.__init__(self, parentNP)
  635. self.colliders = []
  636. self.numColliders = 0
  637. for i in range(numSpheres):
  638. self.addCollider(CollisionSphere(Point3(0), 1))
  639. def addCollider(self, collider):
  640. # Record new collision object
  641. self.colliders.append(collider)
  642. # Add the collider to the collision Node
  643. self.collisionNode.addSolid(collider)
  644. self.numColliders += 1
  645. def setCenter(self, i, center):
  646. c = self.colliders[i]
  647. c.setCenter(center)
  648. def setRadius(self, i, radius):
  649. c = self.colliders[i]
  650. c.setRadius(radius)
  651. def setCenterRadius(self, i, center, radius):
  652. c = self.colliders[i]
  653. c.setCenter(center)
  654. c.setRadius(radius)
  655. def isEntryBackfacing(self, entry):
  656. # If dot product of collision point surface normal and
  657. # ray from sphere origin to collision point is positive,
  658. # center is on the backside of the polygon
  659. fromNodePath = entry.getFromNodePath()
  660. v = Vec3(entry.getSurfacePoint(fromNodePath) -
  661. entry.getFrom().getCenter())
  662. n = entry.getSurfaceNormal(fromNodePath)
  663. # If points almost on top of each other, reject face
  664. # (treat as backfacing)
  665. if v.length() < 0.05:
  666. return 1
  667. # Normalize and check angle between to vectors
  668. v.normalize()
  669. return v.dot(n) >= 0
  670. def pick(self, targetNodePath, skipFlags):
  671. self.ct.traverse(targetNodePath)
  672. self.sortEntries()
  673. return self.findCollisionEntry(skipFlags)
  674. def pickGeom(self, targetNodePath = None,
  675. skipFlags = SKIP_HIDDEN | SKIP_CAMERA):
  676. if targetNodePath is None:
  677. targetNodePath = render
  678. self.collideWithGeom()
  679. return self.pick(targetNodePath, skipFlags)
  680. def pickBitMask(self, bitMask = BitMask32.allOff(),
  681. targetNodePath = None,
  682. skipFlags = SKIP_HIDDEN | SKIP_CAMERA):
  683. if targetNodePath is None:
  684. targetNodePath = render
  685. self.collideWithBitMask(bitMask)
  686. return self.pick(targetNodePath, skipFlags)