panda3d/direct/src/directtools/DirectSelection.py
2001-01-20 02:03:27 +00:00

496 lines
16 KiB
Python

from PandaObject import *
from DirectGeometry import *
from DirectSelection import *
UNPICKABLE = ['x-disc-visible', 'y-disc-visible', 'z-disc-visible',
'GridBack']
# MRM: To do: handle broken node paths in selected and deselected dicts
class DirectNodePath(NodePath):
# A node path augmented with info, bounding box, and utility methods
def __init__(self, nodePath):
# Initialize the superclass
NodePath.__init__(self)
self.assign(nodePath)
# Get a reasonable name
self.name = self.getName()
# Create a bounding box
self.bbox = DirectBoundingBox(self)
center = self.bbox.getCenter()
# Create matrix to hold the offset between the nodepath
# and its center of action (COA)
self.mCoa2Dnp = Mat4()
self.mCoa2Dnp.assign(Mat4.identMat())
# self.mCoa2Dnp.setRow(3, Vec4(center[0], center[1], center[2], 1))
# Transform from nodePath to widget
self.mDnp2Widget = Mat4()
self.mDnp2Widget.assign(Mat4.identMat())
def highlight(self):
self.bbox.show()
def dehighlight(self):
self.bbox.hide()
def getCenter(self):
return self.bbox.getCenter()
def getRadius(self):
return self.bbox.getRadius()
def getMin(self):
return self.bbox.getMin()
def getMax(self):
return self.bbox.getMax()
def __repr__(self):
return ('NodePath:\t%s\n' % self.name)
class SelectedNodePaths(PandaObject):
def __init__(self):
self.reset()
def reset(self):
self.selectedDict = {}
self.deselectedDict = {}
self.last = None
def select(self, nodePath, fMultiSelect = 0):
""" Select the specified node path. Multiselect as required """
# Do nothing if nothing selected
if not nodePath:
print 'Nothing selected!!'
return None
# Reset selected objects and highlight if multiSelect is false
if not fMultiSelect:
self.deselectAll()
# Get this pointer
id = nodePath.id()
# First see if its already in the selected dictionary
dnp = self.getSelectedDict(id)
# If so, we're done
if not dnp:
# See if it is in the deselected dictionary
dnp = self.getDeselectedDict(id)
if dnp:
# Remove it from the deselected dictionary
del(self.deselectedDict[id])
# Show its bounding box
dnp.highlight()
else:
# Didn't find it, create a new selectedNodePath instance
dnp = DirectNodePath(nodePath)
# Show its bounding box
dnp.highlight()
# Add it to the selected dictionary
self.selectedDict[dnp.id()] = dnp
# And update last
self.last = dnp
return dnp
def deselect(self, nodePath):
""" Deselect the specified node path """
# Get this pointer
id = nodePath.id()
# See if it is in the selected dictionary
dnp = self.getSelectedDict(id)
if dnp:
# It was selected:
# Hide its bounding box
dnp.dehighlight()
# Remove it from the selected dictionary
del(self.selectedDict[id])
# And keep track of it in the deselected dictionary
self.deselectedDict[id] = dnp
return dnp
def getSelectedAsList(self):
"""
Return a list of all selected node paths. No verification of
connectivity is performed on the members of the list
"""
return self.selectedDict.values()[:]
def __getitem__(self,index):
return self.getSelectedAsList()[index]
def getSelectedDict(self, id):
"""
Search selectedDict for node path, try to repair broken node paths.
"""
dnp = self.selectedDict.get(id, None)
if dnp:
# Found item in selected Dictionary, is it still valid?
if dnp.verifyConnectivity():
# Yes
return dnp
else:
# Not valid anymore, try to repair
if dnp.repairConnectivity(render):
# Fixed, return node path
return dnp
else:
del(self.selectedDict[id])
return None
else:
# Not in selected dictionary
return None
def getDeselectedAsList(self):
return self.deselectedDict.values()[:]
def getDeselectedDict(self, id):
"""
Search deselectedDict for node path, try to repair broken node paths.
"""
dnp = self.deselectedDict.get(id, None)
if dnp:
# Found item in deselected Dictionary, is it still valid?
if dnp.verifyConnectivity():
# Yes
return dnp
else:
# Not valid anymore, try to repair
if dnp.repairConnectivity(render):
# Fixed, return node path
return dnp
else:
del(self.deselectedDict[id])
return None
else:
# Not in deselected dictionary
return None
def forEachSelectedNodePathDo(self, func):
"""
Perform given func on selected node paths. No node path
connectivity verification performed
"""
selectedNodePaths = self.getSelectedAsList()
for nodePath in selectedNodePaths:
func(nodePath)
def forEachDeselectedNodePathDo(self, func):
"""
Perform given func on deselected node paths. No node path
connectivity verification performed
"""
deselectedNodePaths = self.getDeselectedAsList()
for nodePath in deselectedNodePaths:
func(nodePath)
def getWrtAll(self):
self.forEachSelectedNodePathDo(self.getWrt)
def getWrt(self, nodePath):
nodePath.mDnp2Widget.assign(nodePath.getMat(direct.widget))
def moveWrtWidgetAll(self):
self.forEachSelectedNodePathDo(self.moveWrtWidget)
def moveWrtWidget(self, nodePath):
nodePath.setMat(direct.widget, nodePath.mDnp2Widget)
def deselectAll(self):
self.forEachSelectedNodePathDo(self.deselect)
def highlightAll(self):
self.forEachSelectedNodePathDo(DirectNodePath.highlight)
def dehighlightAll(self):
self.forEachSelectedNodePathDo(DirectNodePath.dehighlight)
def removeSelected(self):
selected = self.last
if selected:
selected.remove()
self.last = None
def removeAll(self):
# Remove all selected nodePaths from the Scene Graph
self.forEachSelectedNodePathDo(NodePath.remove)
def toggleVisSelected(self):
selected = self.last
# Toggle visibility of selected node paths
if selected:
selected.toggleVis()
def toggleVisAll(self):
# Toggle viz for all selected node paths
self.forEachSelectedNodePathDo(NodePath.toggleVis)
def isolateSelected(self):
selected = self.last
if selected:
selected.isolate()
def getDirectNodePath(self, nodePath):
# Get this pointer
id = nodePath.id()
# First check selected dict
dnp = self.getSelectedDict(id)
if dnp:
return dnp
# Otherwise return result of deselected search
return self.getDeselectedDict(id)
def getNumSelected(self):
return len(self.selectedDict.keys())
class DirectBoundingBox:
def __init__(self, nodePath):
# Record the node path
self.nodePath = nodePath
# Compute bounds, min, max, etc.
self.computeBounds()
# Generate the bounding box
self.lines = self.createBBoxLines()
def computeBounds(self):
self.bounds = self.nodePath.getBounds()
if self.bounds.isEmpty():
self.center = Point3(0)
self.radius = 1.0
else:
self.center = self.bounds.getCenter()
self.radius = self.bounds.getRadius()
self.min = Point3(self.center - Point3(self.radius))
self.max = Point3(self.center + Point3(self.radius))
def createBBoxLines(self):
# Create a line segments object for the bbox
lines = LineNodePath(hidden)
lines.node().setName('bboxLines')
lines.setColor( VBase4( 1., 0., 0., 1. ) )
lines.setThickness( 0.5 )
minX = self.min[0]
minY = self.min[1]
minZ = self.min[2]
maxX = self.max[0]
maxY = self.max[1]
maxZ = self.max[2]
# Bottom face
lines.moveTo( minX, minY, minZ )
lines.drawTo( maxX, minY, minZ )
lines.drawTo( maxX, maxY, minZ )
lines.drawTo( minX, maxY, minZ )
lines.drawTo( minX, minY, minZ )
# Front Edge/Top face
lines.drawTo( minX, minY, maxZ )
lines.drawTo( maxX, minY, maxZ )
lines.drawTo( maxX, maxY, maxZ )
lines.drawTo( minX, maxY, maxZ )
lines.drawTo( minX, minY, maxZ )
# Three remaining edges
lines.moveTo( maxX, minY, minZ )
lines.drawTo( maxX, minY, maxZ )
lines.moveTo( maxX, maxY, minZ )
lines.drawTo( maxX, maxY, maxZ )
lines.moveTo( minX, maxY, minZ )
lines.drawTo( minX, maxY, maxZ )
# Create and return bbox lines
lines.create()
# Make sure bbox is never lit or drawn in wireframe
useDirectRenderStyle(lines)
return lines
def updateBBoxLines(self):
ls = self.lines.lineSegs
minX = self.min[0]
minY = self.min[1]
minZ = self.min[2]
maxX = self.max[0]
maxY = self.max[1]
maxZ = self.max[2]
# Bottom face
ls.setVertex( 0, minX, minY, minZ )
ls.setVertex( 1, maxX, minY, minZ )
ls.setVertex( 2, maxX, maxY, minZ )
ls.setVertex( 3, minX, maxY, minZ )
ls.setVertex( 4, minX, minY, minZ )
# Front Edge/Top face
ls.setVertex( 5, minX, minY, maxZ )
ls.setVertex( 6, maxX, minY, maxZ )
ls.setVertex( 7, maxX, maxY, maxZ )
ls.setVertex( 8, minX, maxY, maxZ )
ls.setVertex( 9, minX, minY, maxZ )
# Three remaining edges
ls.setVertex( 10, maxX, minY, minZ )
ls.setVertex( 11, maxX, minY, maxZ )
ls.setVertex( 12, maxX, maxY, minZ )
ls.setVertex( 13, maxX, maxY, maxZ )
ls.setVertex( 14, minX, maxY, minZ )
ls.setVertex( 15, minX, maxY, maxZ )
def getBounds(self):
# Get a node path's bounds
nodeBounds = self.nodePath.node().getBound()
for child in self.nodePath.getChildrenAsList():
nodeBounds.extendBy(child.getBottomArc().getBound())
return nodeBounds.makeCopy()
def show(self):
self.lines.reparentTo(self.nodePath)
def hide(self):
self.lines.reparentTo(hidden)
def getCenter(self):
return self.center
def getRadius(self):
return self.radius
def getMin(self):
return self.min
def getMax(self):
return self.max
def vecAsString(self, vec):
return '%.2f %.2f %.2f' % (vec[0], vec[1], vec[2])
def __repr__(self):
return (`self.__class__` +
'\nNodePath:\t%s\n' % self.nodePath.getName() +
'Min:\t\t%s\n' % self.vecAsString(self.min) +
'Max:\t\t%s\n' % self.vecAsString(self.max) +
'Center:\t\t%s\n' % self.vecAsString(self.center) +
'Radius:\t\t%.2f' % self.radius
)
class SelectionRay:
def __init__(self, camera):
# Create a collision node path attached to the given camera
self.rayCollisionNodePath = camera.attachNewNode( CollisionNode() )
# Don't pay the penalty of drawing this collision ray
self.rayCollisionNodePath.hide()
self.rayCollisionNode = self.rayCollisionNodePath.node()
# Intersect with geometry to begin with
self.collideWithGeom()
# Create a collision ray
self.ray = CollisionRay()
# Add the ray to the collision Node
self.rayCollisionNode.addSolid( self.ray )
# Create a queue to hold the collision results
self.cq = CollisionHandlerQueue()
self.numEntries = 0
# And a traverser to do the actual collision tests
self.ct = CollisionTraverser( RenderRelation.getClassType() )
# Let the traverser know about the queue and the collision node
self.ct.addCollider(self.rayCollisionNode, self.cq )
# List of objects that can't be selected
self.unpickable = UNPICKABLE
def pickGeom(self, targetNodePath = render, fIntersectUnpickable = 0):
self.collideWithGeom()
numEntries = self.pick(targetNodePath,
direct.dr.mouseX,
direct.dr.mouseY)
# Init index
index = -1
# Pick out the closest object that isn't a widget
for i in range(0,numEntries):
entry = direct.iRay.cq.getEntry(i)
node = entry.getIntoNode()
# Don't pick hidden nodes
if node.isHidden():
pass
# Can pick unpickable, use the first visible node
elif fIntersectUnpickable:
index = i
break
# Is it a named node?, If so, see if it has a name
elif issubclass(node.__class__, NamedNode):
name = node.getName()
if name in self.unpickable:
pass
else:
index = i
break
# Not hidden and not one of the widgets, use it
else:
index = i
break
# Did we hit an object?
if(index >= 0):
# Yes!
# Find hit point in camera's space
hitPt = direct.iRay.camToHitPt(index)
hitPtDist = Vec3(hitPt - ZERO_POINT).length()
return (node, hitPt, hitPtDist)
else:
return (None, ZERO_POINT, 0)
def pickWidget(self, targetNodePath = render):
self.collideWithWidget()
numEntries = self.pick(targetNodePath,
direct.dr.mouseX,
direct.dr.mouseY)
# Did we hit a widget?
if numEntries:
# Yes!
# Entry 0 is the closest hit point if multiple hits
minPt = 0
# Find hit point in camera's space
hitPt = direct.iRay.camToHitPt(minPt)
hitPtDist = Vec3(hitPt).length()
# Get the associated collision queue object
entry = direct.iRay.cq.getEntry(minPt)
# Extract the node
node = entry.getIntoNode()
# Return info
return (node, hitPt, hitPtDist)
else:
return (None, ZERO_POINT, 0)
def pick(self, targetNodePath, mouseX, mouseY):
# Determine ray direction based upon the mouse coordinates
# Note! This has to be a cam object (of type ProjectionNode)
self.ray.setProjection( base.cam.node(), mouseX, mouseY )
self.ct.traverse( targetNodePath.node() )
self.numEntries = self.cq.getNumEntries()
self.cq.sortEntries()
return self.numEntries
def collideWithGeom(self):
self.rayCollisionNode.setIntoCollideMask(BitMask32().allOff())
self.rayCollisionNode.setFromCollideMask(BitMask32().allOff())
self.rayCollisionNode.setCollideGeom(1)
def collideWithWidget(self):
self.rayCollisionNode.setIntoCollideMask(BitMask32().allOff())
mask = BitMask32()
mask.setWord(0x80000000)
self.rayCollisionNode.setFromCollideMask(mask)
self.rayCollisionNode.setCollideGeom(0)
def objectToHitPt(self, index):
return self.cq.getEntry(index).getIntoIntersectionPoint()
def camToHitPt(self, index):
# Get the specified entry
entry = self.cq.getEntry(index)
hitPt = entry.getIntoIntersectionPoint()
# Convert point from object local space to camera space
return entry.getInvWrtSpace().xformPoint(hitPt)