Orbiter-Forum  

Go Back   Orbiter-Forum > Orbiter Space Flight Simulator > Orbiter SDK
Register Orbinauts List Social Groups FAQ Projects Mark Forums Read

Orbiter SDK Orbiter software developers post your questions and answers about the SDK, the API interface, LUA, meshing, etc.

Reply
 
Thread Tools
Old 04-13-2020, 08:30 AM   #1
asbjos
tuanibrO
 
asbjos's Avatar
Article Tutorial on how to create animations for 2D panel

This is a tutorial for creating animations in a 2D panel. While I more or less pick up from the last blog post by martins from 2010, this is not to be considered as a continuation of that.
I am no programmer, and there quite possibly are more ways to achieve the same effect. But nevertheless, this is a working solution, and as I believe there is no other tutorial on animations in a 2D panel, I hope this is helpful for somebody.

We are going to go step for step through the process of recreating a basic function from the periscope of the Mercury capsule.
The periscope was used for navigation, so that the astronaut could orient himself in space, even without other working navigational aids.
It was essentially a window with a wide lens, allowing a high field of view, with several marks on the viewfinder.
You can see them in the Mercury Familiarization Guide here.
We are here implementing what is shown on the right hand page (page 12-5, page 355 in PDF), namely the altitude reticles and 5 pitch and roll reticles.

The 5 reticles showed when the capsule was 5 from a 14.5 pitch down 0 roll position.
And if the entire Earth fit within the square of the altitude reticles, the capsule was in a perfect 14.5 pitch down 0 roll attitude.
When in the set orientation, the altitude reticles were a clever solution to allow the astronaut to estimate the altitude within an uncertainty of less than 10 km, something that was useful in a time when communications could be very spotty, and had nothing but himself to determine his state.


We begin where the last blog of martins ended.
We have the current code in clbkLoadPanel2D:
PHP Code:
bool ProjectMercury::clbkLoadPanel2D(int idPANELHANDLE hPanelDWORD viewWDWORD viewH)
{
    const 
DWORD PANEL2D_WIDTH 2160// Panel mesh width, and my screen width
    
const DWORD PANEL2D_HEIGHT 1440// Panel mesh height, and my screen height
    
double defaultScale = (double)viewH PANEL2D_HEIGHT// Scale factor to fit periscope to any user's screen.
    
double panelScale max(defaultScale1.0);
    
SURFHANDLE panelTexture;

    switch (
id)
    {
    case 
0// periscope
        // Here we implement the periscope. F8 to get to periscope, and no on screen information. Seems reasonable

        // First all the FOV and other basic stuff
        
periscope true;

        
SetCameraDefaultDirection(_V(0, -10)); // actual periscope pointed 14.5 deg from nadir, but here we use straight down
        
oapiCameraSetCockpitDir(00); // Rotate camera to desired direction
        
SetCameraRotationRange(0000); // Make camera fixed
        
oapiCameraSetAperture(175.0 RAD 2.0); // actually was 175 deg, but Orbiter only supports up to 160, so it will be truncated.
        // End FOV and other basic stuff

        // Here comes periscope indicators
        
panelTexture oapiGetTextureHandle(periscopeMesh1);

        
SetPanelBackground(hPanel00periscopeMeshPANEL2D_WIDTHPANEL2D_HEIGHT0ULPANEL_ATTACH_TOP PANEL_ATTACH_BOTTOM);

        
SetPanelScaling(hPaneldefaultScalepanelScale);
        
// End periscope indicators
        
        
return true;
    default:
        return 
false;
    }

where periscopeMesh is a MESHHANDLE type, defined as this in the constructor:
PHP Code:
periscopeMesh oapiLoadMeshGlobal("ProjectMercury\\PeriscopeInternalPanel"); 
We should also remove the generic HUD from the periscope view, and do this by overloading clbkRenderHUD:
PHP Code:
void ProjectMercury::clbkRenderHUD(int mode, const HUDPAINTSPEChpsSURFHANDLE hDefaultTex)
{
    if (
periscope)
    {
        return; 
// i.e. supress HUD. Else let HUD show
    
}


Using the mesh attached to this post, you should now have something like this when you are in the correct attitude (level horizon).


That is all good. Now comes the altitude reticle animation. In the Mercury capsule, the astronaut had a knob which could be rotated between 50 and 250 nautical miles (95 and 465 km), and which would set the altitude and 5 reticles accordingly. We'll use a simple keyboard press, increasing or decreasing the altitude by 5 km.
PHP Code:
int ProjectMercury::clbkConsumeBufferedKey(DWORD keybool downcharkstate)
{
    if (!
down) return 0// only process keydown events

    
if (!KEYMOD_CONTROL(kstate) && !KEYMOD_ALT(kstate) && !KEYMOD_SHIFT(kstate)) // No ctrl, alt or shift
    
{
        switch (
key)
        {
        case 
OAPI_KEY_B// Increase
            
if (periscope)
            {
                if (
periscopeAltitude 465.0// 50 to 250 nautical miles, which I convert to 95 to 465 km
                
{
                    
periscopeAltitude += 5.0;

                    
SetPeriscopeAltitude(periscopeAltitude);
                }
            }

            return 
1;
        case 
OAPI_KEY_V:
            if (
periscope)
            {
                if (
periscopeAltitude 95.0// 50 to 250 nautical miles, which I convert to 95 to 465 km
                
{
                    
periscopeAltitude -= 5.0;

                    
SetPeriscopeAltitude(periscopeAltitude);
                }
            }

            return 
1;
        }
    }
    return 
0;


Now, we can set the altitude, but we have done nothing with the animations yet. It happens in our function SetPeriscopeAltitude. We define it like so:

We actually do not animate per se, but instead edit the mesh itself on the fly, using oapiEditMeshGroup.
If you look in the PeriscopeInternalPanel.msh mesh file, you can see that the mesh groups that we want to edit are the first eight (four sides of altitude reticle square and four sides of 5 reticle square, in the following order: BOX0_RIGHT, BOX5_RIGHT, BOX0_DOWN, BOX5_DOWN, BOX0_LEFT, BOX5_LEFT, BOX0_UP, BOX5_UP).
When we edit the mesh group, we are going to totally replace the x-positions of the left and right reticles (move to the sides), and similarly replace the y-positions of the up and down reticles (move vertically).

We just start with the right altitude reticle (BOX0_RIGHT).
It is defined in the mesh file like this:
Code:
LABEL BOX0_RIGHT
MATERIAL 0
TEXTURE 1
GEOM 4 2
1495.0 1440.0 0.0000 0.0000 0.0000 -1.000 0.6000 0.0000 ; centered at pixel 1500 in x, stretching from bottom (1440 pixel in y)
1495.0 0.0000 0.0000 0.0000 0.0000 -1.000 1.0000 0.0000 ; 											to top (0 pixel in y)
1505.0 0.0000 0.0000 0.0000 0.0000 -1.000 0.6000 0.4000
1505.0 1440.0 0.0000 0.0000 0.0000 -1.000 1.0000 0.4000 ; thus reticle width is 10 pixels (1505 - 1495 = 10).	
0 1 2 ; the two triangles making up the reticle
0 2 3
We define the function SetPeriscopeAltitude like this:
PHP Code:
void ProjectMercury::SetPeriscopeAltitude(double inputAltitude)
{
    const 
double reticleWidth 10.0// width of our reticle in pixels.

    
const double centreX 2160.0 2.0// my screen and mesh is width 2160, so centre is half of that
    
const double centreY 1440.0 2.0// my screen and mesh is height 1440, so centre is half of that
    
    
const int box0rightGrp 0// mesh group index of BOX0_RIGHT
    
const int totalVertices 4// we are editing a total of four vertices.
    
    
WORD vertexIndex[totalVertices] = { 0123}; // index of the four vertices. Simply all four.
    
GROUPEDITSPEC gesRight0;
    
gesRight0.flags GRPEDIT_VTXCRDX// We're editing the x-coordinate of the vertex.
    
gesRight0.nVtx totalVertices// number of vertices we're editing, 4.
    
gesRight0.vIdx vertexIndex// the indices of the four vertices: {0, 1, 2, 3}
    
    
NTVERTEX newVertexRight[totalVertices]; // This will be where we define the four new vertices.
    
    
double vertexDisplacement0 8.0e3 pow(inputAltitude, -0.52);
    
    
newVertexRight[0].float(centreX reticleWidth 2.0 vertexDisplacement0);
    
newVertexRight[1].float(centreX reticleWidth 2.0 vertexDisplacement0); // position is centre of screen, minus the width of reticle, and finally the displacement
    
newVertexRight[2].float(centreX reticleWidth 2.0 vertexDisplacement0); // position is centre of screen, plus the width of reticle, and finally the displacement
    
newVertexRight[3].float(centreX reticleWidth 2.0 vertexDisplacement0);
    
    
gesRight0.Vtx newVertexRight// load in the four new vertex x-positions
    
    
oapiEditMeshGroup(periscopeMeshbox0rightGrp, &gesRight0); // Do the magic. This is where we actually perform the mesh transformation.
    

    // Add debug string to show how we're doing
    
sprintf(oapiDebugString(), "Altitude input: %.1f km. VertexDisplacement0: %.1f pixels. Actual altitude: %.3f km"inputAltitudevertexDisplacement0GetAltitude() / 1e3);

You may notice that I've simply added the generic pixel displacement vertexDisplacement0. We want the reticle to be positioned so that it aligns with the position of the Earth's horizon on our screen. This is simply a task of calibration, and I've done it for you already.
The pixel displacement of the horizon of the Earth from screen centre is quite accurately described by the equation (inputAltitude in km), which in C++ is written as
PHP Code:
double vertexDisplacement0 8.0e3 pow(inputAltitude, -0.52); 
The pixel distance from centre of screen to Earth's horizon when the orientation is 5 off-centre, is by the way given by
PHP Code:
double vertexDisplacement5 2.4e4 pow(inputAltitude, -0.65); 
Now, if we compile and run, we should get something like the screenshot below.


If we increase the altitude, and fit the reticle to the horizon again, we get this.

Pretty darn good!

We're almost there!
First, we need to implement the same group edits for the remaining 7 reticles. One can then just copy and paste the preceding SetPeriscopeAltitude code seven times. But remember to change from GRPEDIT_VTXCRDX to GRPEDIT_VTXCRDY when editing BOX0_DOWN, BOX5_DOWN, BOX0_UP and BOX5_UP, and also changing from NTVERTEX x component to y component! I've spent a few hours trying to fix my problems, when it was simply this small blunder.

You may want to generalise this as you're basically doing the same thing eight times. Here's what I ended up with (I've outsourced vertexDisplacement0 to its own function):
PHP Code:
void ProjectMercury::SetPeriscopeAltitude(double inputAltitude)
{
    const 
int totalGroupNumber 8;

    
//                                             0r     5r     0d     5d     0l     5l     0u     5u
    
const int reticleGroup[totalGroupNumber] = { 0123456};

    const 
double reticleWidth 10.0;

    const 
double centreX 2160.0 2.0;
    const 
double centreY 1440.0 2.0;

    const 
int totalVertices 4;
    static 
WORD vertexIndex[totalVertices] = { 012};

    
double vertexDisplacement0vertexDisplacement5;
    
GetPixelDeviationForAltitude(periscopeAltitude, &vertexDisplacement0, &vertexDisplacement5);

    for (
int i 0totalGroupNumberi++)
    {
        
double displacement vertexDisplacement0;
        if (
== 1displacement vertexDisplacement5// 5 degree displacement

        
double displacementSign 1.0;
        if (
>= 4displacementSign = -1.0// negative coordinate

        
GROUPEDITSPEC ges;
        
NTVERTEX newVertex[totalVertices];

        if (
== || == || == || == 7// y
        
{
            
ges.flags GRPEDIT_VTXCRDY;
            
ges.nVtx totalVertices;
            
ges.vIdx vertexIndex;

            
newVertex[0].float(centreY reticleWidth 2.0 displacementSign displacement);
            
newVertex[1].float(centreY reticleWidth 2.0 displacementSign displacement);
            
newVertex[2].float(centreY reticleWidth 2.0 displacementSign displacement);
            
newVertex[3].float(centreY reticleWidth 2.0 displacementSign displacement);

            
ges.Vtx newVertex;
            
oapiEditMeshGroup(periscopeMeshreticleGroup[i], &ges);
        }
        else
        {
            
ges.flags GRPEDIT_VTXCRDX;
            
ges.nVtx 4;
            
ges.vIdx vertexIndex;

            
newVertex[0].float(centreX reticleWidth 2.0 displacementSign displacement);
            
newVertex[1].float(centreX reticleWidth 2.0 displacementSign displacement);
            
newVertex[2].float(centreX reticleWidth 2.0 displacementSign displacement);
            
newVertex[3].float(centreX reticleWidth 2.0 displacementSign displacement);

            
ges.Vtx newVertex;
            
oapiEditMeshGroup(periscopeMeshreticleGroup[i], &ges);
        }
    }
}

void ProjectMercury::GetPixelDeviationForAltitude(double inputAltitudedouble *deg0Pixdouble *deg5Pix)
{
    
// We assume exponential dependency. Calibrated by taking screenshots at different heights, and counting pixels.

    
if (inputAltitude == NULLinputAltitude 160.0// Default value (and perigee altitude of Mercury capsule)
    
    // Limits from Mercury Familiarization Guide page 362 (chapter 12-5)
    
if (inputAltitude 50.0 1.852inputAltitude 50.0 1.852;
    if (
inputAltitude 250.0 1.852inputAltitude 250.0 1.852;

    *
deg0Pix 8.0e3 pow(inputAltitude, -0.52);
    *
deg5Pix 2.4e4 pow(inputAltitude, -0.65);


Finally, you'll maybe notice that when you load a scenario, the periscope reticles will be uninitialised until you actually adjust the inputAltitude.
This is fixed by calling the SetPeriscopeAltitude function after Orbiter has finished loading:

PHP Code:
void ProjectMercury::clbkVisualCreated(VISHANDLE visint refcount)
{
    
// Initialise periscope
    
SetPeriscopeAltitude(periscopeAltitude);


Congratulations! You have completed this exercise!
If you want to go into even more detail of what I've done, the entire source code is included in the Project Mercury X addon. The functions we have covered today are inside the MercuryCapsule.h, MercuryAtlas.cpp and VirtualCockpit.h files, with the definitions located in MercuryAtlas.h.

Attached Files
File Type: zip PeriscopeInternalPanel.zip (1.1 KB, 1 views)

Last edited by asbjos; 04-18-2020 at 06:45 AM. Reason: Grammar
asbjos is offline   Reply With Quote
Old 04-17-2020, 05:29 PM   #2
Observation
Orbinaut
Default

Great! I have actually ended up making a MeshAnimation class because you can't always use loops if the panel gets more complex. Then after having it intialized with the translation vector, the detail on the vertices to transform and even an initialState parameter to get fancy. The constructor the stores the initial (0) state and I only make a call to setAnimationState(double state) at runtime.
Another note is that you can do this either with vertex or texture coordinates, whatever fits your purpose.

Should we add some more things like sketchpad use and then post it all together over on the tutorials page?
Observation is offline   Reply With Quote
Old 04-18-2020, 06:44 AM   #3
asbjos
tuanibrO
 
asbjos's Avatar
Default

I would love to see your (or anyone else's!) implementation of this, so please do publish your attempt too!
I've only used sketchpad with clbkDrawHUD so far, so I have no experience with it for panels.

I contemplated whether to publish here (OrbiterSDK page) or on the tutorials page. The tutorial page has mostly Orbiter flying tutorials, so I decided to post it here instead. But it could be misplaced. I now see Hlynkacg's great "Coding a Lunar Lander from the ground up" tutorial is there, so it may not be so misplaced after all.

If any moderator/administrator wants to move this post, then feel free to do so.
asbjos is offline   Reply With Quote
Old 04-20-2020, 04:36 PM   #4
Observation
Orbinaut
Default

Ok, so here I go.

I will show you how to make dynamic elements, like MFD buttons, for which the content is created at runtime. Or at least, I will show you how I did it...

You will find all the files necessary for this here. It's a working add-on, though it is very basic. Source code can be found in Orbitersdk/samples. And of course, thank you to crisbeta for the mesh from RSB.

Let's start:

First, we load the panel mesh with something like this:
PHP Code:
// global variable
static const float PANEL_WIDTH 1280;
static const 
float PANEL_HEIGHT 400;

// constructor
panel_mesh NULL;

// destructor
if (panel_meshoapiDeleteMesh(panel_mesh);

// loadMainPanel(...)
if (panel_meshoapiDeleteMesh(panel_mesh);
panel_mesh oapiLoadMesh("TestAddOn/panel");

SetPanelBackground(hPanel, &PANEL_STATIC_TEXTURE1panel_mesh
     
static_cast<DWORD>(PANEL_WIDTH), static_cast<DWORD>(PANEL_HEIGHT), 
     
0PANEL_ATTACH_BOTTOM PANEL_MOVEOUT_BOTTOM); 
Since the static texture we use doesn't ever change, we can make it a global variable:
PHP Code:
// definition
static SURFHANDLE PANEL_STATIC_TEXTURE NULL;

// ovcInit(...)
{
     
PANEL_STATIC_TEXTURE oapiLoadTexture("TestAddOn/panel_elements.dds");
     ...
}
// ovcExit(...)
{
     if (
PANEL_STATIC_TEXTUREoapiDestroySurface(PANEL_STATIC_TEXTURE);
     ...

With panel_mesh being a class attribute MESHHANDLE.
The panel is very basic: a dark blue background with yet non-existing MFDs and buttons.

And the texture this is built from is here:

The background is really just one pixel, and we have our active and inactive buttons for later use.
As you can see if you have a look at the .msh file, you will see that the every button is defined individually. That would be quite a lot of work if you wanted to do it by hand. So don't be shy and use a panel mesh generator when you start having even so slightly complex meshes.

Let's now register the two dedicated mesh groups as MFDs. This is done simply with:
PHP Code:
RegisterPanelMFDGeometry(hPanelMFD_LEFT01);
RegisterPanelMFDGeometry(hPanelMFD_RIGHT02); 
If you rewrite the MFD vertices one day, be careful to keep the indices in a precise order: 0 is top left, 1 is top right, 2 is bottom left and 3 is bottom right. Otherwise, the MFD will be rotated or even unreadable.
So now our panel looks like something:


Then, we need to create a texture for our dynamic elements. We will start with the 24 MFD buttons in a 12 x 2 layout. So we have:
PHP Code:
// constructor
panel_dynamic_texture NULL;

// destructor
if (panel_meshoapiDeleteMesh(panel_mesh);

// loadMainPanel(...)
if (panel_dynamic_textureoapiDestroySurface(panel_dynamic_texture);
panel_dynamic_texture oapiCreateSurfaceEx(12 MFD_BUTTON_TEXWIDTH
     
MFD_BUTTON_TEXHEIGHT
     
OAPISURFACE_SKETCHPAD OAPISURFACE_UNCOMPRESS OAPISURFACE_TEXTURE); 
Where panel_dynamic_texture is a class atribute SURFHANDLE, MFD_BUTTON_TEXWIDTH and MFD_BUTTON_TEXHEIGHT are one button's width and height in pixels. OAPISURFACE_SKETCHPAD will allow us to use the sketchpad later on and OAPISURFACE_UNCOMPRESS will keep the surface uncompressed because drawing on compressed surfaces is computationally expensive.
Before calling SetPanelBackground(), we have to pack both textures together in an array:
PHP Code:
SURFHANDLE panelTextures[] = { PANEL_STATIC_TEXTUREpanel_dynamic_texture }; 
And we set the nsurf argument as 2:
PHP Code:
SetPanelBackground(hPanelpanelTextures2panel_mesh
     
static_cast<DWORD>(PANEL_WIDTH), static_cast<DWORD>(PANEL_HEIGHT), 
     
0PANEL_ATTACH_BOTTOM PANEL_MOVEOUT_BOTTOM); 
You might also find it interesting to know that at the end of the .msh file we have
Code:
MATERIALS 0
TEXTURES 2
in order for this to work.

Now, we will want to draw the buttons from the static texture into the dynamic texture. But if we do so at every frame, we might see the frame rate burn up. So we want to know when we have to redraw a button. As a bonus, since we have active and inactive states for the buttons, it would be neat to know which button is pressed. Therefore, we need two new class attributes:
PHP Code:
// global variables
static const DWORD MFD_COLUMN_NBTS 6;

// class definition
...
bool change_mfdButton[MFD_COLUMN_NBTS];
int pressed_mfdButton;

// constructor
pressed_mfdButton = -1;     // for none

// loadMainPanel(...)
...
for (
int i 0MFD_COLUMN_NBTSi++)
{
    
change_mfdButton[i] = true;

Where MFD_COLUMN_NBTS is the number of buttons we have in one column. We best set all change_mfdButton to true each time we reload the panel.
But we are not the only ones requesting updates, in the sense that the contents of the MFD might suddenly change when we switch to another MFD mode. Luckily, the VESSEL interface has a function just for that:
PHP Code:
void TestAddOn::clbkMFDMode(int mfdint mode)
{
    if (
mfd == MFD_LEFT)
    {
        for (
int i 0MFD_COLUMN_NBTSi++)
        {
            
change_mfdButton[i] = true;
        }
    }
    else
    {
        for (
int i MFD_COLUMN_NBTSMFD_COLUMN_NBTSi++)
        {
            
change_mfdButton[i] = true;
        }
    }

Now we can finally start drawing them. We will need a new member function to do that:
PHP Code:
// global variables
static RECT PANEL_MFD_BUTTON_INACTIVE = { 007550 };
static 
RECT PANEL_MFD_BUTTON_ACTIVE = { 05075100 };

// class definition
...
void updatePanel()
...
// implementation
void TestAddOn::updatePanel()
{
    for (
int y 02y++)
    {
        for (
int x 0MFD_COLUMN_NBTS 2x++)
        {
            
int buttonID MFD_COLUMN_NBTS x;
            if (
change_mfdButton[buttonID])
            {
                
change_mfdButton[buttonID] = false;
                
RECT tgt = { MFD_BUTTON_TEXWIDTHMFD_BUTTON_TEXHEIGHT, (1) * MFD_BUTTON_TEXWIDTH, (1) * MFD_BUTTON_TEXHEIGHT };
                if (
buttonID == pressed_mfdButton)
                {
                    
oapiBlt(panel_dynamic_texturePANEL_STATIC_TEXTURE, &tgt, &PANEL_MFD_BUTTON_ACTIVE);
                }
                else
                {
                    
oapiBlt(panel_dynamic_texturePANEL_STATIC_TEXTURE, &tgt, &PANEL_MFD_BUTTON_INACTIVE);
                }
            }
        }
    }

It's all pretty straightforward here. We haven't done anything with pressed_mfdButton yet, but we can already put it there. PANEL_MFD_BUTTON_ACTIVE and PANEL_MFD_BUTTON_INACTIVE are RECTs that define the area in the static texture where we take the active and inactive buttons. Note that we do not need to change the vertices in any way because we change the underlying texture.
Then, we call it from clbkPreStep(...):
PHP Code:
if (oapiCockpitMode() == COCKPIT_PANELS)   // only if we're in panel view
{
    
updatePanel();

The dynamic texture now looks like this:

And the panel now has buttons. Unusable, sure, but it does have buttons.

Orbiter actually has a built in function that is called whenever a certain area needs to be redrawn: clbkPanelRedraw(...). It is also perfectly valid to implement that one and set the PANEL_REDRAW_... parameter in RegisterPanelArea(...), which we will see in a second. But I prefer this way of doing because:
- it allows more flexibility: you can iterate through all buttons once instead of doing it 4 times separately for the 4 separate panel areas
- it gives you access to the whole surface (I have had issues with textures being more precise than the mesh coordinates and PANEL_MAP_DIRECT does not seem to work
But you are of course free to do whatever you want.

Let's add labels to our buttons. We start by retrieving the label of the button we're working on:
PHP Code:
...
const 
charlabel oapiMFDButtonLabel((== 0) ? MFD_LEFT MFD_RIGHTx); 
If there is no label, this function returns NULL. If we have to display a label, we then retrieve our sketchpad:
PHP Code:
if (label)
{
    
oapi::Sketchpadsketchpad oapiGetSketchpad(panel_dynamic_texture);
...
oapiReleaseSketchpad(sketchpad);

The sketchpad is the tool that will allow us to write text or paint other stuff on our surface. When the sketchpad is in use, blitting may not work. So always remember to call oapiReleaseSketchpad(...) when you are finished with it.
We yet have to create a font for it:
PHP Code:
// definition
static oapi::FontPANEL_FONT_MFD_BUTTON NULL;

// ovcInit(...)
PANEL_FONT_MFD_BUTTON oapiCreateFont(37false"Sans"FONT_BOLD);

// ovcExit(...)
if (PANEL_FONT_MFD_BUTTONoapiReleaseFont(PANEL_FONT_MFD_BUTTON); 
About the face parameter, the documentation says the following:
Quote:
The following generic typeface names should be understood by all graphics systems:
• Fixed (fixed pitch font)
• Sans (sans-serif proportional font)
• Serif (serif proportional font) Other font names may not be recognised by all graphics clients. In that
case, the default fixed or sans-serif font will be used, depending on the value of prop.
We can then attach it to the sketchpad, along with other parameters:
PHP Code:
sketchpad->SetFont(PANEL_FONT_MFD_BUTTON);
sketchpad->SetTextColor(0xFFFFFF);
sketchpad->SetTextAlign(oapi::Sketchpad::CENTERoapi::Sketchpad::BASELINE); 
The sketchpad might be used by other components that will eventually change font, color and other parameters (typically MFDs), so you will have to set them before each use.
And we can finally write our text:
PHP Code:
sketchpad->Text(static_cast<int>(tgt.left MFD_BUTTON_TEXWIDTH 2.0f), static_cast<int>(tgt.top + (tgt.bottom tgt.top) * 7.0f), labelstrlen(label)); 
tgt is still there from the blit operation. You might have to tweak the values a little when you add something new. 5 / 7 isn't scientific, it just looks good. And normally, we should now have this:


The last thing we have to do is to have our buttons respond to mouse input. For this, we need to register certain panel areas. That is, we tell Orbiter that for an arbitrary rectangle of our screen, we want to recieve mouse event notifications. It is done as follows in the loadMainPanel(...) function:
PHP Code:
// global variables
static const int  PANEL_MFD_LEFT_LEFT_BUTTONS_ID 0;
static 
RECT PANEL_MFD_LEFT_LEFT_BUTTONS_RECT = { 26047340 };
static const 
int  PANEL_MFD_LEFT_RIGHT_BUTTONS_ID 1;
static 
RECT PANEL_MFD_LEFT_RIGHT_BUTTONS_RECT = { 35360398340 };
static const 
int  PANEL_MFD_RIGHT_LEFT_BUTTONS_ID 2;
static 
RECT PANEL_MFD_RIGHT_LEFT_BUTTONS_RECT = { 88360928340 };
static const 
int  PANEL_MFD_RIGHT_RIGHT_BUTTONS_ID 3;
static 
RECT PANEL_MFD_RIGHT_RIGHT_BUTTONS_RECT = { 1232601277340 };

// loadMainPanel(...)
const RECT nr = { 000};
RegisterPanelArea(hPanelPANEL_MFD_LEFT_LEFT_BUTTONS_ID
     
PANEL_MFD_LEFT_LEFT_BUTTONS_RECT2nrPANEL_REDRAW_NEVER
     
PANEL_MOUSE_LBDOWN PANEL_MOUSE_LBUP PANEL_MOUSE_LBPRESSED
     
PANEL_MAP_NONE);
RegisterPanelArea(hPanelPANEL_MFD_LEFT_RIGHT_BUTTONS_ID
     
PANEL_MFD_LEFT_RIGHT_BUTTONS_RECT2nrPANEL_REDRAW_NEVER
     
PANEL_MOUSE_LBDOWN PANEL_MOUSE_LBUP PANEL_MOUSE_LBPRESSED
     
PANEL_MAP_NONE);
RegisterPanelArea(hPanelPANEL_MFD_RIGHT_LEFT_BUTTONS_ID
     
PANEL_MFD_RIGHT_LEFT_BUTTONS_RECT2nrPANEL_REDRAW_NEVER
     
PANEL_MOUSE_LBDOWN PANEL_MOUSE_LBUP PANEL_MOUSE_LBPRESSED
     
PANEL_MAP_NONE);
RegisterPanelArea(hPanelPANEL_MFD_RIGHT_RIGHT_BUTTONS_ID
     
PANEL_MFD_RIGHT_RIGHT_BUTTONS_RECT2nrPANEL_REDRAW_NEVER
     
PANEL_MOUSE_LBDOWN PANEL_MOUSE_LBUP PANEL_MOUSE_LBPRESSED
     
PANEL_MAP_NONE); 
The function takes as parameters:
  1. the panel
  2. an ID for the area that will be passed to the callback functions
  3. the rectangle for which to generate events in mesh coordinates
  4. a rectangle to the area of the texture that should be passed to clbkPanelRedrawEvent(...) (ignored)
  5. the index of the texture this rectangle refers to (ignored)
  6. when to call clbkPanelRedrawEvent(...)
  7. when to call clbkPanelMouseEvent(...)
  8. what surface to map for clbkPanelRedrawEvent(...) (ignored)
We then implement clbkPanelMouseEvent(...):
PHP Code:
// class definition
bool clbkPanelMouseEvent(int idint eventint mxint myvoidcontext);

// implementation
bool TestAddOn::clbkPanelMouseEvent(int idint eventint mxint myvoidcontext)
{
    switch (
id)
    {
    ...
    }

Here we recieve:
  1. the ID of the area that recieved an event
  2. the event that generated the call (it will always be one or a combination of the flags set at RegisterPanelArea(...))
  3. the position with respect to the top and left of the specified rectangle
  4. a context pointer that is left over from an other implementation of the same function
I decided to generalize the implementation a bit:
PHP Code:
//global variables
static const DWORD MFD_BUTTON_YDIST 50;

static const 
LONG MFD_BUTTON_WIDTH 45;    // okay, it's not used, but keep it *class*
static const LONG MFD_BUTTON_HEIGHT 30;

//clbkPanelMouseEvent(...)
switch (id)
{
case 
PANEL_MFD_LEFT_LEFT_BUTTONS_ID:    // process them all together
case PANEL_MFD_LEFT_RIGHT_BUTTONS_ID:
case 
PANEL_MFD_RIGHT_LEFT_BUTTONS_ID:
case 
PANEL_MFD_RIGHT_RIGHT_BUTTONS_ID:
    if ((
event PANEL_MOUSE_LBDOWN) == PANEL_MOUSE_LBDOWN && (my MFD_BUTTON_YDIST) < MFD_BUTTON_HEIGHT)
    {
        
pressed_mfdButton static_cast<int>(floor(static_cast<float>(my) / MFD_BUTTON_YDIST));
        switch (
id)
        {
        case 
PANEL_MFD_LEFT_RIGHT_BUTTONS_ID:
            
pressed_mfdButton += MFD_COLUMN_NBTS;
            break;
        case 
PANEL_MFD_RIGHT_LEFT_BUTTONS_ID:
            
pressed_mfdButton += MFD_COLUMN_NBTS;
            break;
        case 
PANEL_MFD_RIGHT_RIGHT_BUTTONS_ID:
            
pressed_mfdButton += MFD_COLUMN_NBTS;
            break;
        default:
            break;
        }
        
change_mfdButton[pressed_mfdButton] = true;
    }
    if (
pressed_mfdButton != -1)
    {
        
oapiProcessMFDButton((pressed_mfdButton MFD_COLUMN_NBTS) ? MFD_LEFT MFD_RIGHT,
            
pressed_mfdButton - ((pressed_mfdButton MFD_COLUMN_NBTS) ? : (MFD_COLUMN_NBTS)), event);
        if ((
event PANEL_MOUSE_LBUP) == PANEL_MOUSE_LBUP)
        {
            
change_mfdButton[pressed_mfdButton] = true;
            
pressed_mfdButton = -1;
        }
    }
    break;
default: 
    return 
false;
    break;
}
return 
true
Since the buttons are spaced out 50 units each but are only 30 units high, if the rest of the division by 50 of the my parameter is bigger than 30, we are not hitting a button. If we are, we find out which one we are hitting (when pressing the button down). If we are currently pressing a button, we let Orbiter process it and watch out for the release of the button. The event flag might be a combination of multiple bitflags, thus we need to verify if it contains the desired flag and not if it is equal ((event & ...) == ...). And if we processed the event, we return true to indicate that we did so.

With that done, the buttons should be working. Of course, you are still missing the power, selection and menu buttons, but I thought I'd leave them to you as an exercise. They have only been commented out in the Panel Mesh Generator (in Meshes/TestAddOn/).

I hope that I have been able to explain this clearly. I would really appreciate comments and corrections, and hope we will see more 2D panels in future add-ons ;-).


Cheers!!

Thomas

Last edited by Observation; 04-21-2020 at 12:26 PM.
Observation is offline   Reply With Quote
Reply

Tags
2d panel, animations, mesh, tutorial


Thread Tools

Posting Rules
BB code is On
Smilies are On
[IMG] code is On
HTML code is Off
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts
Forum Jump


All times are GMT. The time now is 12:15 PM.

Quick Links Need Help?


About Us | Rules & Guidelines | TOS Policy | Privacy Policy

Orbiter-Forum is hosted at Orbithangar.com
Powered by vBulletin® Version 3.8.11
Copyright ©2000 - 2020, vBulletin Solutions Inc.
Copyright 2007 - 2017, Orbiter-Forum.com. All rights reserved.
X vBulletin 3.8.11 Debug Information
  • Page Generation 0.14146 seconds
  • Memory Usage 5,796KB
  • Queries Executed 13 (?)
More Information
Template Usage:
  • (1)SHOWTHREAD
  • (1)ad_footer_end
  • (1)ad_footer_start
  • (1)ad_header_end
  • (1)ad_header_logo
  • (1)ad_navbar_below
  • (1)ad_showthread_beforeqr
  • (1)ad_showthread_firstpost
  • (1)ad_showthread_firstpost_sig
  • (1)ad_showthread_firstpost_start
  • (2)bbcode_code
  • (27)bbcode_php
  • (1)bbcode_quote
  • (1)footer
  • (1)forumjump
  • (1)forumrules
  • (1)gobutton
  • (1)header
  • (1)headinclude
  • (1)navbar
  • (3)navbar_link
  • (41)option
  • (1)postbit_attachment
  • (4)postbit_legacy
  • (4)postbit_onlinestatus
  • (4)postbit_wrapper
  • (5)showthread_similarthreadbit
  • (1)showthread_similarthreads
  • (1)spacer_close
  • (1)spacer_open
  • (4)tagbit
  • (1)tagbit_wrapper 

Phrase Groups Available:
  • global
  • inlinemod
  • postbit
  • posting
  • reputationlevel
  • showthread
Included Files:
  • ./showthread.php
  • ./global.php
  • ./includes/init.php
  • ./includes/class_core.php
  • ./includes/config.php
  • ./includes/functions.php
  • ./includes/class_hook.php
  • ./includes/functions_bigthree.php
  • ./includes/class_postbit.php
  • ./includes/class_bbcode.php
  • ./includes/functions_reputation.php 

Hooks Called:
  • init_startup
  • init_startup_session_setup_start
  • init_startup_session_setup_complete
  • cache_permissions
  • fetch_postinfo_query
  • fetch_postinfo
  • fetch_threadinfo_query
  • fetch_threadinfo
  • fetch_foruminfo
  • style_fetch
  • cache_templates
  • global_start
  • parse_templates
  • global_setup_complete
  • showthread_start
  • showthread_getinfo
  • forumjump
  • showthread_post_start
  • showthread_query_postids
  • showthread_query
  • bbcode_fetch_tags
  • bbcode_create
  • showthread_postbit_create
  • postbit_factory
  • postbit_display_start
  • fetch_musername
  • bbcode_parse_start
  • postbit_imicons
  • bbcode_parse_complete_precache
  • bbcode_parse_complete
  • postbit_attachment
  • postbit_display_complete
  • tag_fetchbit
  • tag_fetchbit_complete
  • showthread_similarthread_query
  • showthread_similarthreadbit
  • forumrules
  • navbits
  • navbits_complete
  • showthread_complete