Custom GUI Controls

Hi everybody,

I’m new to Panda3d and I’m trying to implement the following: I have a mechanical arm 3d model with several joints; I’d like to control these joints with a set of custom UI controls to put in aspect2D (eg. a draggable slider controls with custom 3D appearance), but I am not sure how I should proceed.

  • Shall I customize the “look and feel” of existing DirectGUI controls, like DirectSlider? In such case, where can I find instructions on how this can be done?
  • Do I have to create my DirectGuiWidget(s)? Again, do some instructions or tutorials exist for this?
  • Shall I just reparent 3D models to aspect2D then implement mouse actions (eg. dragging) with the P3D collision detection mechanism?

Thanks in advance!

The third option is suitable as a solution to your problem, I think you need to implement the gizmo principle to work in 3D. As far as I know, panda does not have a ready-made solution for controlling through the 3D world.

The only thing I don’t understand is why you should use aspect2d for this, it doesn’t make any sense.

Greetings, and welcome to the forum! I hope that you find your time here to be positive! :slight_smile:

So, to address your questions:

Ultimately, this is up to you–up to whether you want to customise the controls, whether you want to spend the time and other resources on doing so, and so on.

The short version is that it’s largely done via parameters provided to the various widgets, which should be found listed in the manual, here:
https://docs.panda3d.org/1.10/python/programming/gui/directgui/index

(Note that the pages for the various widgets I believe only list those parameters specific to those widgets, with parameters introduced by parent-classes being listed in full on the pages for those classes (or the main page covering DirectGUI. So, for example, a DirectButton accepts a variety of parameters specific to DirectButton–but also accepts parameters specific to its parent-class, DirectFrame.)

That’s the usual way, indeed. I think that the manual has short sample-code examples on the pages for the various widgets.

However, there is an alternative: a community-member created a tool called “DirectGUI Designer” which might make life easier:

(Although let me note that I haven’t used it myself, and so am not in a position to speak to its quality one way or another.)

You don’t need to reparent your models to aspect2d should you desire to do this, I believe: you can use various tools provided by the engine (such as the collision system) to determine what the mouse is being pointed at, how it has been moved, and to then update the state of the models accordingly.

For an example, see the manual’s section on “Clicking on 3D Objects” (which should be linked-to below).
https://docs.panda3d.org/1.10/python/programming/collision-detection/clicking-on-3d-objects

(That said, I don’t recommend in general that one have the ray collide with visible geometry, as that example does by its use of “GeomNode.getDefaultCollideMask”–for scenes and objects that aren’t simple, doing so can impact performance I believe. Instead, I generally suggest the use of dedicated collision geometry.

Of course, if your scene is very simple, then colliding with visible geometry may well work for you!)

Thanks,

Thanks for the suggestions and the quick reply!

The reason I want to put them in aspect2D is that I want to have something like: the 3d scene on the “central” part of the main window, with a 2D UI control panel taking the right side of the screen with the controls on top, as in the screenshot below.

I guess from your answer that among the alternatives the best way is to implement my own DirectGuiWidget subclass: I didn’t find any specific instruction on the manual on how to do this, but see that the source code is well documented, so I’ll take a look at that. Thanks again!

At the moment, nothing prevents you from using a slider attached to aspect2d and a model that is located on the render node.

Aaaah, I see! Well, in that case, parenting a copy of your model below aspect2d (or more likely, below the control-panel that holds it) might well be a good way to go about it.

You might have to manually apply depth-testing to it (as I think that aspect2d by default doesn’t do depth-testing), but it should otherwise work, I believe.

If you just want to drag the mouse in order to move the model, then you probably don’t need a sub-class: you should be able to receive mouse-events directly from the engine, or use the “bind” method provided by DirectGUI widgets to receive them from simple DirectFrames.

(Noting that, if you do use DirectFrames, you may have to set their state to “DGG.NORMAL” in order to have them send events.)

That said, a sub-class might produce neater and more-maintainable code, perhaps. (Depending in part on what coding style is most congenial to you, I daresay.)

Thanks to @logan and his post DirectSlider with progressBar I managed to have the effect I wanted.

The code still needs some cleaning up (for instance it doesn’t handle the horizontal vs. vertical case…), but I’m almost there… I post it below here in case it might be useful to someone else.

There are two points of improvements in particular that’s I’m struggling with:

  1. When I pass the reference to command to the ControlSlider options, I need to rely on a dictionary where I define in the container control (ControlPanel) and pass this together with the slider’s name as extraArgs to the showValue function. I couldn’t think of a more elegant way to realize this, clearly I can’t do something like slider = ControlSlider( ... command=command, extraArgs=slider ) in addSliderControl because the variable slider isn’t initialized yet. Can someone suggest any better solution?

  2. The .png images I’m using for the control have an Alpha channel with transparency to allow for rounded corners. Unfortunately I couldn’t find a way to make the ControlPanel background show trough it. Any suggestions?

from direct.gui.DirectGui import *
from panda3d.core import *
import direct.directbase.DirectStart
from panda3d.core import Vec4
from direct.gui.DirectGui import DGG,DirectButton,DirectSlider, OnscreenImage
from panda3d.core import TransparencyAttrib
	
class ControlPanel(DirectFrame):
	
    def __init__(self, application):
        self.application = application
        super().__init__(frameColor=(1, 0, 0, 1), pos=(1.5, 0, 0))
	
        background_cm = CardMaker("image_plane")
        background_cm.set_frame(-1, 1, -1, 1)
        self.background_plane = self.attach_new_node(background_cm.generate())
        self.background_plane.setColor((.85, .85, .85, 1))
	
        self.allSliders = {}
	
    def addSliderControl( self, image, pos_x, pos_y, scale, command, name ):
	
        slider = ControlSlider(
            image=image,
            pos=(pos_x,0,pos_y),
            scale=scale,
            value=50, 
            range=[0,100],
            command=command,
            extraArgs=[name, self.allSliders]
            )
	
        # self.attach_new_node(slider)
        slider.reparentTo(self)
        print(f"slider: {slider.getPos()}")
        
        self.allSliders[name] = slider
	
	
class ControlSlider(DirectSlider):
    
    def __init__(self, parent = None, **kw):
	
        optiondefs = (
            ('allowprogressBar', True, self.__progressBar),
            ('image', None, None)
            )
	
        background_texture = self.createImageTexture(kw['image'])
        image_width, image_height = self.getImageTextureDims(background_texture)
        image_aspect_ratio = image_height / image_width
        print(f"image_aspect_ratio = {image_aspect_ratio}")
        
        optiondefs += (
                ('frameSize',      (-1, 1, -.5, .5),   None),
                ('frameVisibleScale', (1, 1),         None),        
                ('image_scale', (1, 1, image_aspect_ratio), None),
                ('orientation', DGG.HORIZONTAL, None ),
                ('relief', DGG.FLAT, None ),
                ('progressBar_relief', DGG.FLAT, None ),
                ('progressBar_frameColor', (1, 1, 1, .5), None ),
                ('progressBar_frameTexture', 'models/textures/blue.PNG', None ),
                ('thumb_relief', DGG.FLAT, None ),
                ('thumb_frameColor', (1.0,1.0,1.0,0), None )
        )
	
        self.orientation = None
        self.progressBar = None
        self.defineoptions(kw, optiondefs)
	
        # Initialize superclasses
        DirectSlider.__init__(self, parent)
        
        self.progressBar = self.createcomponent("progressBar", (), None,
                                          DirectButton, (self,),
                                          borderWidth = self['borderWidth'],
                                          state=DGG.DISABLED,
                                          sortOrder=-1)
        
	
        # Call option initialization functions
        self.initialiseoptions(ControlSlider)
	
        self.setTransparency(TransparencyAttrib.MAlpha)
        self.progressBar.setTransparency(TransparencyAttrib.MAlpha)
        self.component('image0').setTransparency(TransparencyAttrib.MAlpha)
        
        
    def createImageTexture(self, image):
	
        tex = loader.load_texture(image)
        
        # Enable transparency on the texture
        tex.set_format(Texture.F_rgba)
        tex.set_wrap_u(Texture.WM_clamp)
        tex.set_wrap_v(Texture.WM_clamp)
        tex.set_minfilter(Texture.FT_linear_mipmap_linear)
        tex.set_magfilter(Texture.FT_linear)
	
        return tex
    
	
    def getImageTextureDims(self, image_tex):
        image_width = image_tex.get_x_size()
        image_height = image_tex.get_y_size()
	
        return image_width, image_height
    
    # see code at https://discourse.panda3d.org/t/directslider-with-progressbar/29126
    # for the following methods
	
    def __progressBar(self):
       ...
        
    def __updProgressBar(self):
       ...
    
    #override
    def setOrientation(self):
        ...
        
    #override
    def destroy(self):
        ...
	
    #override
    def commandFunc(self):
        ...
	
    #override
    def setFrameSize(self, fClearFrame = 0):
        ...
	
	

	
def showValue(name, sliders):
    print(f">>>{name}: {sliders[name]['value']}")
	
control_panel = ControlPanel(base)
control_panel.addSliderControl("../models/textures/ctrl_base_heading.png", 
                                            -0.35, 0.1, 0.5,
                                            command=showValue,
                                            name="base_heading"
                                            )
control_panel.addSliderControl("../models/textures/ctrl_1st_arm_heading.png", 
                                            -0.35, 0.65, 0.5,
                                            command=showValue,
                                            name="1st_arm_heading")
	
	
base.setBackgroundColor(0.8, 0.2, 0.2, 1.0)  # R, G, B, A
base.run()

image02

If I understand your problem correctly, then what I’ve usually done is to simply set the “extraArgs” after initialisation. Something like this:

myGUIWidget = SomeDirectGUIClass(
                                 # ... parameters here as per usual ...
                                )
myGUIWidget["extraArgs"] = [myGUIWidget]

As a word of caution for this sort of thing, I’d recommend clearing “extraArgs” when cleaning up your widget! I don’t know whether DirectGUI does any such itself, but if it doesn’t then you may be left with a circular reference that may interfere with Python’s garbage collection.

I think that if you set the “relief” keyword-parameter to “None” (instead of “DGG.FLAT”), you should find that the image should appear and transparency work as expected.

(I’m not sure offhand of whether you have to explicitly apply transparency with a call to “setTransparency”; if doing the above without it fails, then perhaps try adding that call.)

Both suggestion worked! Thanks a lot for your support :smiley: !

For the image, setting ‘relief’ to None was sufficient to enable transparency.

Now I’m faced with the hardest problem of having the progressBar (the shaded part in overaly of the image) being transparent where the underlying image is.

image03

1 Like

It’s my pleasure! I’m glad to have been of help! :slight_smile:

As to your new issue, hmm… Perhaps a few texture-stages, where one of the stages masks based on the underlying image, and another is used as a mask to depict the current degree of progress (so that you don’t have to change the size of the progress bar itself)?

But perhaps someone else will have a better idea!