module for individual camera control

This is a module for individual camera control. The subject is a visualization, which is a diagram on the plane Y = 0, so the cameras always point down the Y axis (or up it). The arrow keys move the camera parallel to the plane. The mouse left-button selects a region for zooming in. The mouse right-button grabs & scrolls or drags. The wheel zooms in or out by a proportion. Multiple cameras operate individually; the keys control the camera of whatever view the mouse is currently over. The View and ViewGroup classes should be de-coupled from other dependencies by now.

if __name__== '__main__':
    import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject
from panda3d.core import *
import random as ran

class ViewGroup(DirectObject):
    def __init__(self):
        self.accept("arrow_up", self.route, ['scroll3'] )
        self.accept("arrow_down", self.route, ['scroll4'] )
        self.accept("arrow_left", self.route, ['scroll1'] )
        self.accept("arrow_right", self.route, ['scroll2'] )
        self.accept("arrow_up-repeat", self.route, ['scroll3'] )
        self.accept("arrow_down-repeat", self.route, ['scroll4'] )
        self.accept("arrow_left-repeat", self.route, ['scroll1'] )
        self.accept("arrow_right-repeat", self.route, ['scroll2'] )
        self.accept("page_up", self.route, ['zoom1'] )
        self.accept("page_down", self.route, ['zoom2'] )
        self.accept("page_up-repeat", self.route, ['zoom1'] )
        self.accept("page_down-repeat", self.route, ['zoom2'] )
        self.accept("wheel_up", self.route, ['zoom1'] )
        self.accept("wheel_down", self.route, ['zoom2'] )
        self.accept("mouse1", self.route, ['mouse1','mouse1','down'] )
        self.accept("mouse1-up", self.route, ['mouse1up','mouse1','up'] )
        self.accept("mouse3", self.route, ['mouse3','mouse3','down'] )
        self.accept("mouse3-up", self.route, ['mouse3up','mouse3','up'] )
        self.accept('window-event', self.windowEvent )

        self.prevreg= { }
        self.views= [ ]
    def addViews( self, *views ):
        for view in views:
            self.views.append( view )
    def windowEvent( self, win ):
        for v in self.views:
            v.rezoom( relzoom= 1 )
    def route( self, methName, *args ):
        if not base.mouseWatcherNode.hasMouse( ):
            return
        if args and args[1]== 'up':
            reg= self.prevreg.pop( args[0], None )
            if reg is None:
                return
            args= args[2:]
        else:
            x=base.mouseWatcherNode.getMouseX()
            y=base.mouseWatcherNode.getMouseY()
            reg= base.mouseWatcherNode.getOverRegion( x, y )
        if reg is None:
            return
        if args and args[1]== 'down':
            self.prevreg[ args[0] ]= reg
            args= args[2:]
        if not reg.getName( ).startswith( 'region-' ):
            return
        view= int( reg.getName( )[ 7 ] )
        view= self.views[ view ]
        getattr( view, methName )( *args )

class View( ):
    mouseselR= LineSegs( 'mousesel' ) # where I really want to put this...
    mouseselR.setColor( 0, 0, 0, 1 )
    mouseselY= 0
    mouseselR.moveTo( 0, mouseselY, 0 )
    mouseselR.drawTo( 1, mouseselY, 0 )
    mouseselR.drawTo( 1, mouseselY, 1 )
    mouseselR.drawTo( 0, mouseselY, 1 )
    mouseselR.drawTo( 0, mouseselY, 1 )
    mouseselN= mouseselR.create( )
    mousesel= NodePath( mouseselN )
    mousesel.reparentTo( render2d )
    mousesel.hide( )

    def __init__( self, i, rect, zoom0, pos0 ):
        self.lens= OrthographicLens( )
        self.cam= Camera( 'cam', self.lens )
        self.dispreg= base.win.makeDisplayRegion( *rect )
        self.dispreg.setClearColorActive( True )
        r, g= ran.uniform( .8, 1 ), ran.uniform( .8, 1 )
        b= 2.5- r- g
        self.dispreg.setClearColor( VBase4( r, g, b, 1 ) )
        self.camP= NodePath( self.cam )
        self.dispreg.setCamera( self.camP )
        self.camP.reparentTo( render )
        self.camP.setPos( *pos0 )
        self.rezoom( zoom0 )
        self.mousereg= MouseWatcherRegion( 'region-%i'%i, *[ x*2-1 for x in rect ] )
        base.mouseWatcherNode.addRegion( self.mousereg )
        self.mousewatN= MouseWatcher( 'mousewatcher-%i'%i )
        self.mousewatN.setDisplayRegion( self.dispreg )
        self.mousewat= NodePath( self.mousewatN )
        self.mousewat.reparentTo( base.mouseWatcher.getParent( ) )

        self.mouse1x1=self.mouse1x2=None
        self.mouse3p= None
    def reshape( self, rect ):
        self.dispreg.setDimensions( *rect )
        self.mousereg.setFrame( *[ x*2-1 for x in rect ] )
        self.rezoom( relzoom= 1 )
    def zoom1( self ):
        self.rezoom( relzoom= 1/ .825 )
    def zoom2( self ):
        self.rezoom( relzoom= .825 )
    def rezoom( self, zoom= None, relzoom= None ):
        if zoom is not None:
            self.zoom= zoom
        else:
            self.zoom= zoom= self.zoom* relzoom
        x= self.dispreg.getPixelWidth( )/ float( zoom )
        y= self.dispreg.getPixelHeight( )/ float( zoom )
        self.lens.setFilmSize( x, y )
    def scroll1( self ):
        self.camP.setX( self.camP, -1 )
    def scroll2( self ):
        self.camP.setX( self.camP, 1 )
    def scroll3( self ):
        self.camP.setZ( self.camP, 1 )
    def scroll4( self ):
        self.camP.setZ( self.camP, -1 )
    def screen_to_yplane( self, x, y ):
        a, b, c= Point3( ), Point3( ), Point3( )
        self.lens.extrude( Point2( x, y ), a, b )
        a= NodePath( ).getRelativePoint( self.camP, a )
        b= NodePath( ).getRelativePoint( self.camP, b )
        p= Plane( Point3( 0,0,0 ), Point3( 1,0,0 ), Point3( 0,0,1 ) )
        p.intersectsLine( c, a, b )
        return c
    def mouse1( self ):
        if not self.mousewatN.hasMouse():
            return
        taskMgr.add( self.dragging1, 'dragging1')
        self.mouse1x1, self.mouse1y1= self.mousewatN.getMouse( )
        self.mouse1vx1, self.mouse1vy1= base.mouseWatcherNode.getMouse()
        self.mousesel.show( )
    def mouse1up( self ):
        taskMgr.remove( 'dragging1' )
        self.mousesel.hide( )
        if self.mouse1x1 is None or self.mouse1x2 is None:
            return
        if self.mouse1x1==self.mouse1x2 or self.mouse1y1==self.mouse1y2:
            return
        p00= self.screen_to_yplane( self.mouse1x1, self.mouse1y1 )
        p11= self.screen_to_yplane( self.mouse1x2, self.mouse1y2 )
        x3, y3, z3= ( p00+ p11 )/ 2
        self.camP.setX( x3 )
        self.camP.setZ( z3 )
        xz= self.dispreg.getPixelWidth( )/ abs( p00[ 0 ]- p11[ 0 ] )
        zz= self.dispreg.getPixelHeight( )/ abs( p00[ 2 ]- p11[ 2 ] )
        zoom= max( xz, zz )
        self.rezoom( zoom )
        self.mouse1x1= self.mouse1x2= None
    def dragging1( self, task ):
        if not self.mousewatN.hasMouse():
            return task.cont
        self.mouse1x2, self.mouse1y2= self.mousewatN.getMouse()
        self.mouse1vx2, self.mouse1vy2= base.mouseWatcherNode.getMouse()
        self.mouseselR.setVertex( 0, self.mouse1vx1, self.mouseselY, self.mouse1vy1 )
        self.mouseselR.setVertex( 1, self.mouse1vx2, self.mouseselY, self.mouse1vy1 )
        self.mouseselR.setVertex( 2, self.mouse1vx2, self.mouseselY, self.mouse1vy2 )
        self.mouseselR.setVertex( 3, self.mouse1vx1, self.mouseselY, self.mouse1vy2 )
        self.mouseselR.setVertex( 4, self.mouse1vx1, self.mouseselY, self.mouse1vy1 )
        return task.cont
    def mouse3( self ):
        if not self.mousewatN.hasMouse():
            return
        taskMgr.add( self.dragging3, 'dragging3')
        x1, y1= self.mousewatN.getMouse( )
        self.mouse3p= self.screen_to_yplane( x1, y1 )
    def mouse3up( self ):
        taskMgr.remove( 'dragging3' )
        self.mouse3p= None
    def dragging3( self, task ):
        if not self.mousewatN.hasMouse():
            return task.cont
        x2, y2= self.mousewatN.getMouse( )
        p2= self.screen_to_yplane( x2, y2 )
        x3, y3, z3= self.mouse3p- p2
        self.camP.setX( self.camP, x3 )
        self.camP.setZ( self.camP, z3 )
        return task.cont

if __name__== '__main__':
    ''' demo for panda3d.org forums '''
    cm= CardMaker( 'cardmaker' )
    c= NodePath( cm.generate( ) )
    c.reparentTo( render )
    base.cam.removeNode( )

    viewgroup= ViewGroup()
    viewgroup.addViews( View( 0, ( 0, 1, 0, .5 ), 50, ( 5, -2, 3 ) ),
                        View( 1, ( .25, 1, .5, 1 ), 20, ( 5, -2, 3 ) ) )
    
    def press1( ):
        viewgroup.views[ 1 ].reshape( ( ran.uniform( .1, .7 ), 1, .5, 1 ) )
    base.accept( '1', press1 )

    run( )

If you can use this, great. If it can use something you have, great.

there is a typo in your code

could you provide a little usage script (using i.e. the classic hello world snippet) to test it?

Sure. It should be above. Nothing so fancy as ‘hello’ and ‘world’, but a white square. The views are created in the executable section properly, and the ‘mousesel’ LineSegs object is included in the View class. (Strictly, it should be in ViewGroup, but then Views would have to know what group they’re in, and there needs to be a way to declare that relation as such.)

Lastly, the original hsl2rgb function bit the dust on this one, for the backgrounds of different views; we got a cheap substitute instead. Though it does make a decent standard.

looks nice and useful - are you going to develop a map editor with this?

It seems a shame to “waste” an entire class on something that will only have two instances. Information visualization is notoriously hard. But what types of diagram could use more than two zoom levels of the whole?

The selection-box does seem to specialize in 2-D diagrams, because the corners aren’t defined in a 3-D space, and because we’re just adjusting the film size and not the camera-Y, which both are possible in 3-D. Some 3-D applications may have solutions to these both.

Another popular strategy for scrolling is the grab-and-coast, which could be made an option or preferred for some purposes.

Further controls, such as sizing and positioning, and adding and removing views, aren’t present. The ‘reshape’ method was just added. Controls might include sashes and GUI-style layout and packers and MDI.

How did you find the scroll-in/-out of the mouse wheel? Should not page-up/-down have corresponding behavior and not reversed?

Maps are important given some assumptions. One example is drawing a curve in an editor or picking a waypoint for a route, where you want to see the local shape and global shape at once; or perhaps where you want something to be just out of range of something else. More dynamic interfaces could open more options to game designers, since token ranges could exceed one “screenful”. Perhaps you want to keep an eye on something, by fixing a view to a moving target or terrain.

Visualization of large tables and charts could be important. Spreadsheet programs have capabilities to hide and show regions selectively or view in split-screen. You could view one column and another side-by-side. I understand that timetables are a classic example which could benefit. Or maybe you think your paint program should have it.

Anyway, I think I kept it pretty minimal but it has a lot of potential. It always seems to be a staple when you picture someone using software.