Thanks again ynjh_jo. I’ve now got the integration of zoom_to_node working, and I’ve also integrated the zooming back to home and the mouse clicking from your code. I still don’t totally understand the zoom_to_node code, but that’s the only bit, and it’s all working.
Next, I plan to implement some simple layout nodes. For example, a node that lays out all the child nodes you add to it in a neat horizontal or vertical line, or in a grid formation, etc. I know you already implemented a grid layout in this thread with a simple for-loop, I’m basically planning to generalise that a little.
Here’s the code I have so far:
# Todo: implement layouts, and have a demo that uses a bunch of
# objects in a layout.
# Use NodePath.getChildren().asList():
# <https://discourse.panda3d.org/viewtopic.php?t=4120&highlight=getchildren>
# Also this manual page:
# <http://panda3d.org/manual/index.php/Searching_the_Scene_Graph>
from pandac.PandaModules import *
import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject
from direct.interval.IntervalGlobal import *
from direct.gui.DirectGui import *
from direct.task import Task
import sys
wp = WindowProperties()
wp.setSize(1024,768)
base.win.requestProperties(wp)
# The coordinate space of aspect 2D is (l,r,b,t):
# (-aspRatio,aspRatio,-1,1)
# (You can go beyond this coordinate space, but you'll be outside
# the Panda3D window.)
aspRatio=base.getAspectRatio()
# Put a bunch of stuff on aspect2d.
# Objects are placed at (0,0) if no pos is specified.
ost = OnscreenText(text='Hello!')
# Warning! Image files should be a power of 2 in size:
# 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048 ...
# otherwise Panda3D will scale and possibly stretch them.
# As long as it's a power of 2 and square an image seems to get
# scaled only, not stretched.
# Trying to get an image with OnscreenImage to appear the same
# pixel size as it is in file seems to be impossible.
# To avoid these difficulties with OnscreenImage you should use
# egg-texture-cards instead:
# <https://discourse.panda3d.org/viewtopic.php?t=3467&highlight=egg+texture+card>
#
# When scaling and positioning on aspect2d, the Y value is
# irrelevant, it should always be 1 when scaling and 0 when
# positioning.
osi = OnscreenImage(image='house.png',scale=.3,pos=(-.5,0,.5))
# And some DirectGUI objects...
node = aspect2d.attachNewNode('n')
button = DirectButton(text = ("OK", "click!", "rolling over", "disabled"), scale=.1, pos=(.5,0,.5))
checkButton = DirectCheckButton(text = "CheckButton" ,scale=.05, pos=(0,0,-.5))
entry = DirectEntry(scale=.05,initialText="Write on me!", numLines=2, pos=(.25,0,.75))
label = DirectLabel(scale=.05,text="DirectLabel",pos=(-1,0,0))
cm = CardMaker('card')
width,height = 0.088320312, 0.1328125
left,right,bottom,top = 0-0.5*width,0.5*width,0-0.5*height,0.5*height
width = right - left
height = top - bottom
cm.setFrame(left,right,bottom,top)
np = aspect2d.attachNewNode(cm.generate())
tex = loader.loadTexture('house.png')
np.setTexture(tex,1)
np.setPos(-1,0,-1)
# Set the collide mask that will make these objects clickable.
aspect2d.setCollideMask(BitMask32.bit(3))
class Canvas(DirectObject):
def __init__(self):
self.accept("escape",sys.exit)
self.dummy=render2d.attachNewNode('dummy')
aspect2d.wrtReparentTo(self.dummy)
self.accept("x", self.zoom_out)
self.accept("x-up",self.stop_zoom_out)
self.accept("z",self.zoom_in)
self.accept("z-up",self.stop_zoom_in)
self.accept("arrow_left",self.pan_left)
self.accept("arrow_left-up",self.stop_pan_left)
self.accept("arrow_right",self.pan_right)
self.accept("arrow_right-up",self.stop_pan_right)
self.accept("arrow_up",self.pan_up)
self.accept("arrow_up-up",self.stop_pan_up)
self.accept("arrow_down",self.pan_down)
self.accept("arrow_down-up",self.stop_pan_down)
self.accept('space',self.zoom_to_home)
self.zoom_interval = None
self.zoom_time = 2 # Time (in seconds) that it takes to
# zoom all the way from the minimum
# zoom level to the max or vice-versa.
self.max_zoom = 4.0
self.min_zoom = .1
self.pan_x_interval = None
self.pan_x_time = 2
self.max_pan_x = 1.33
self.min_pan_x = -1.33
# In Panda the Z axis represents up. (Y is forward.)
self.pan_z_interval = None
self.pan_z_time = 2
self.max_pan_z = 1.0
self.min_pan_z = -1.0
# Create a CollisionRay and attach it to aspect2d.
cn = CollisionNode('mouseRay')
cn.addSolid(CollisionRay(0,-100,0, 0,1,0))
# The ray will collide into anything with a matching mask.
# NOTE: This means that if you want an object to be
# clickable, you have to give it this collide mask.
cn.setFromCollideMask(BitMask32.bit(3))
# But nothing will collide into the ray.
cn.setIntoCollideMask(BitMask32.allOff())
# Keep a reference to the NodePath of the CollisionNode.
self.cnp = aspect2d.attachNewNode(cn)
# Now we need a collision traverser and collision handler.
self.ctrav=CollisionTraverser()
self.queue = CollisionHandlerQueue()
# Add our collision node to the traverser, and associate
# it with our handler.
self.ctrav.addCollider(self.cnp, self.queue)
# For debugging only.
self.ctrav.showCollisions(aspect2d)
# Last, we need a task function to check for any
# collisions and update selection if the mouse is over
# a node.
taskMgr.add(self.mouse,'mouse')
self.selection = None # Keeps track of which NodePath the
# mouse pointer is over.
# When the mouse is clicked, we zoom in on the nodepath
# that the mouse pointer is over (if any)/
self.accept('mouse1',self.click)
def mouse(self,t):
"""Check for collisions and update self.selection if the
mouse pointer is over a nodepath."""
# If the mouse pointer is not inside the window do nothing
if not base.mouseWatcherNode.hasMouse():
return Task.cont
# Move the collision ray's nodepath to where the mouse
# pointer is.
mpos = base.mouseWatcherNode.getMouse()
self.cnp.setPos(render2d,mpos[0],0,mpos[1])
# Check whether the collision ray hits anything.
self.ctrav.traverse(aspect2d)
# If the ray collides with anything, point self.selection
# to the first nodepath that the ray collides with.
# Otherwise set self.selection to None.
if self.queue.getNumEntries():
self.selection = self.queue.getEntry(0).getIntoNodePath()
else:
self.selection = None
return Task.cont
def click(self):
"""
If self.selection is not None and we are not already
zooming, initiate a zoom_to_node on self.selected.
"""
if self.selection is not None:
self.zoom_to_node(self.selection)
def zoom_in(self):
if self.zoom_interval is not None:
if self.zoom_interval.isPlaying():
self.zoom_interval.pause()
current_zoom = self.dummy.getScale().getY()
duration = ((self.max_zoom-current_zoom)/self.max_zoom)*self.zoom_time
self.zoom_interval = self.dummy.scaleInterval(
duration=duration,
scale=4,
name = "zoom_in")
self.zoom_interval.start()
def zoom_out(self):
if self.zoom_interval is not None:
if self.zoom_interval.isPlaying():
self.zoom_interval.pause()
current_zoom = self.dummy.getScale().getY()
duration = ((current_zoom-self.min_zoom)/self.max_zoom)*self.zoom_time
self.zoom_interval = self.dummy.scaleInterval(
duration=.5,
scale=.1,
name = "zoom_out")
self.zoom_interval.start()
def stop_zoom_in(self):
if self.zoom_interval is not None:
if self.zoom_interval.isPlaying() and self.zoom_interval.getName() == "zoom_in":
self.zoom_interval.pause()
def stop_zoom_out(self):
if self.zoom_interval is not None:
if self.zoom_interval.isPlaying() and self.zoom_interval.getName() == "zoom_out":
self.zoom_interval.pause()
def pan_left(self):
if self.pan_x_interval is not None:
if self.pan_x_interval.isPlaying():
self.pan_x_interval.pause()
current_pan = self.dummy.getPos().getX()
duration = ((self.max_pan_x-current_pan)/self.max_pan_x)*self.pan_x_time
self.pan_x_interval = self.dummy.posInterval(
duration=duration,
pos = Vec3(self.max_pan_x,self.dummy.getPos().getY(),self.dummy.getPos().getZ()),
name = "pan_left")
self.pan_x_interval.start()
def pan_right(self):
if self.pan_x_interval is not None:
if self.pan_x_interval.isPlaying():
self.pan_x_interval.pause()
current_pan = self.dummy.getPos().getX()
duration = ((current_pan-self.min_pan_x)/self.max_pan_x)*self.pan_x_time
self.pan_x_interval = self.dummy.posInterval(
duration=duration,
pos = Vec3(self.min_pan_x,self.dummy.getPos().getY(),self.dummy.getPos().getZ()),
name = "pan_right")
self.pan_x_interval.start()
def stop_pan_left(self):
if self.pan_x_interval is not None:
if self.pan_x_interval.isPlaying() and self.pan_x_interval.getName() == "pan_left":
self.pan_x_interval.pause()
def stop_pan_right(self):
if self.pan_x_interval is not None:
if self.pan_x_interval.isPlaying() and self.pan_x_interval.getName() == "pan_right":
self.pan_x_interval.pause()
def pan_down(self):
if self.pan_z_interval is not None:
if self.pan_z_interval.isPlaying():
self.pan_z_interval.pause()
current_pan = self.dummy.getPos().getZ()
duration = ((self.max_pan_z-current_pan)/self.max_pan_z)*self.pan_z_time
self.pan_z_interval = self.dummy.posInterval(
duration=duration,
pos = Vec3(self.dummy.getPos().getX(),self.dummy.getPos().getY(),self.max_pan_z),
name = "pan_down")
self.pan_z_interval.start()
def pan_up(self):
if self.pan_z_interval is not None:
if self.pan_z_interval.isPlaying():
self.pan_z_interval.pause()
current_pan = self.dummy.getPos().getZ()
duration = ((current_pan-self.min_pan_z)/self.max_pan_z)*self.pan_z_time
self.pan_z_interval = self.dummy.posInterval(
duration=duration,
pos = Vec3(self.dummy.getPos().getX(),self.dummy.getPos().getY(),self.min_pan_z),
name = "pan_up")
self.pan_z_interval.start()
def stop_pan_up(self):
if self.pan_z_interval is not None:
if self.pan_z_interval.isPlaying() and self.pan_z_interval.getName() == "pan_up":
self.pan_z_interval.pause()
def stop_pan_down(self):
if self.pan_z_interval is not None:
if self.pan_z_interval.isPlaying() and self.pan_z_interval.getName() == "pan_down":
self.pan_z_interval.pause()
def zoom_to_node(self,np):
# ynjh_jo's zoom code. This is why we have the dummy node.
# We move the dummy node to the center of the object we're
# zooming in on, and use it as a target for aspect2d.
a2dOldTransf=aspect2d.getTransform(render2d)
self.dummy.setPos(np,0,0,0)
aspect2d.setTransform(render2d,a2dOldTransf)
pos = np.getPos(render2d)
scale = np.getScale(aspect2d)
aspect_ratio = scale[0]/scale[2]
bounds3 = np.getTightBounds()
bounds = render2d.getRelativeVector(np.getParent(),bounds3[1]-bounds3[0])
if aspect_ratio < base.getAspectRatio():
maxScale=2./bounds[2]
else:
maxScale=2./bounds[0]
self.zoom_to_interval = self.dummy.posHprScaleInterval(
duration = .5,
pos = self.dummy.getPos() - pos,
hpr = self.dummy.getHpr(),
scale = self.dummy.getScale() * maxScale
)
self.zoom_to_interval.start()
def zoom_to_home(self):
"""Zoom back to the original view."""
a2dOldTransf=aspect2d.getTransform(render2d)
self.dummy.clearTransform()
aspect2d.setTransform(render2d,a2dOldTransf)
self.zoom_home_interval = aspect2d.posHprScaleInterval(
duration = .5,
pos = Point3(0,0,0),
hpr = aspect2d.getHpr(),
scale = Vec3(1/base.getAspectRatio(),1,1)
)
self.zoom_home_interval.start()
c = Canvas()
run ()