Discussion Tutorial on how to create animations for 2D panel

asbjos

tuanibrO
Addon Developer
Joined
Jun 22, 2011
Messages
696
Reaction score
259
Points
78
Location
This place called "home".
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:
bool ProjectMercury::clbkLoadPanel2D(int id, PANELHANDLE hPanel, DWORD viewW, DWORD 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(defaultScale, 1.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, -1, 0)); // actual periscope pointed 14.5 deg from nadir, but here we use straight down
		oapiCameraSetCockpitDir(0, 0); // Rotate camera to desired direction
		SetCameraRotationRange(0, 0, 0, 0); // 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(periscopeMesh, 1);

		SetPanelBackground(hPanel, 0, 0, periscopeMesh, PANEL2D_WIDTH, PANEL2D_HEIGHT, 0UL, PANEL_ATTACH_TOP | PANEL_ATTACH_BOTTOM);

		SetPanelScaling(hPanel, defaultScale, panelScale);
		// End periscope indicators
		
		return true;
	default:
		return false;
	}
}
where periscopeMesh is a MESHHANDLE type, defined as this in the constructor:
PHP:
periscopeMesh = oapiLoadMeshGlobal("ProjectMercury\\PeriscopeInternalPanel");

We should also remove the generic HUD from the periscope view, and do this by overloading clbkRenderHUD:
PHP:
void ProjectMercury::clbkRenderHUD(int mode, const HUDPAINTSPEC* hps, SURFHANDLE 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).
InitialView.png


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:
int ProjectMercury::clbkConsumeBufferedKey(DWORD key, bool down, char* kstate)
{
	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:
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] = { 0, 1, 2, 3}; // 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].x = float(centreX - reticleWidth / 2.0 + vertexDisplacement0);
	newVertexRight[1].x = float(centreX - reticleWidth / 2.0 + vertexDisplacement0); // position is centre of screen, minus the width of reticle, and finally the displacement
	newVertexRight[2].x = float(centreX + reticleWidth / 2.0 + vertexDisplacement0); // position is centre of screen, plus the width of reticle, and finally the displacement
	newVertexRight[3].x = float(centreX + reticleWidth / 2.0 + vertexDisplacement0);
	
	gesRight0.Vtx = newVertexRight; // load in the four new vertex x-positions
	
	oapiEditMeshGroup(periscopeMesh, box0rightGrp, &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", inputAltitude, vertexDisplacement0, GetAltitude() / 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 [math]8000 \times inpAlt^{-0.52}[/math] (inputAltitude in km), which in C++ is written as
PHP:
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:
double vertexDisplacement5 = 2.4e4 * pow(inputAltitude, -0.65);

Now, if we compile and run, we should get something like the screenshot below.
Right0View160.png


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

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:
void ProjectMercury::SetPeriscopeAltitude(double inputAltitude)
{
	const int totalGroupNumber = 8;

	//											 0r	 5r	 0d	 5d	 0l	 5l	 0u	 5u
	const int reticleGroup[totalGroupNumber] = { 0, 1, 2, 3, 4, 5, 6, 7 };

	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] = { 0, 1, 2, 3 };

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

	for (int i = 0; i < totalGroupNumber; i++)
	{
		double displacement = vertexDisplacement0;
		if (i % 2 == 1) displacement = vertexDisplacement5; // 5 degree displacement

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

		GROUPEDITSPEC ges;
		NTVERTEX newVertex[totalVertices];

		if (i == 2 || i == 3 || i == 6 || i == 7) // y
		{
			ges.flags = GRPEDIT_VTXCRDY;
			ges.nVtx = totalVertices;
			ges.vIdx = vertexIndex;

			newVertex[0].y = float(centreY + reticleWidth / 2.0 + displacementSign * displacement);
			newVertex[1].y = float(centreY + reticleWidth / 2.0 + displacementSign * displacement);
			newVertex[2].y = float(centreY - reticleWidth / 2.0 + displacementSign * displacement);
			newVertex[3].y = float(centreY - reticleWidth / 2.0 + displacementSign * displacement);

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

			newVertex[0].x = float(centreX - reticleWidth / 2.0 + displacementSign * displacement);
			newVertex[1].x = float(centreX - reticleWidth / 2.0 + displacementSign * displacement);
			newVertex[2].x = float(centreX + reticleWidth / 2.0 + displacementSign * displacement);
			newVertex[3].x = float(centreX + reticleWidth / 2.0 + displacementSign * displacement);

			ges.Vtx = newVertex;
			oapiEditMeshGroup(periscopeMesh, reticleGroup[i], &ges);
		}
	}
}

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

	if (inputAltitude == NULL) inputAltitude = 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.852) inputAltitude = 50.0 * 1.852;
	if (inputAltitude > 250.0 * 1.852) inputAltitude = 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:
void ProjectMercury::clbkVisualCreated(VISHANDLE vis, int 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.

FinalView.png
 

Attachments

  • PeriscopeInternalPanel.zip
    1.1 KB · Views: 2
Last edited:

Observation

New member
Joined
Jun 13, 2019
Messages
26
Reaction score
5
Points
3
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?
 

asbjos

tuanibrO
Addon Developer
Joined
Jun 22, 2011
Messages
696
Reaction score
259
Points
78
Location
This place called "home".
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. :hmm:

If any moderator/administrator wants to move this post, then feel free to do so.
 

Observation

New member
Joined
Jun 13, 2019
Messages
26
Reaction score
5
Points
3
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:
// global variable
static const float PANEL_WIDTH = 1280;
static const float PANEL_HEIGHT = 400;

// constructor
panel_mesh = NULL;

// destructor
if (panel_mesh) oapiDeleteMesh(panel_mesh);

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

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

// ovcInit(...)
{
     PANEL_STATIC_TEXTURE = oapiLoadTexture("TestAddOn/panel_elements.dds");
     ...
}
// ovcExit(...)
{
     if (PANEL_STATIC_TEXTURE) oapiDestroySurface(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.
blank-panel.png

And the texture this is built from is here:
panel-elements.png

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:
RegisterPanelMFDGeometry(hPanel, MFD_LEFT, 0, 1);
RegisterPanelMFDGeometry(hPanel, MFD_RIGHT, 0, 2);
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:
panel-MFDs.png


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:
// constructor
panel_dynamic_texture = NULL;

// destructor
if (panel_mesh) oapiDeleteMesh(panel_mesh);

// loadMainPanel(...)
if (panel_dynamic_texture) oapiDestroySurface(panel_dynamic_texture);
panel_dynamic_texture = oapiCreateSurfaceEx(12 * MFD_BUTTON_TEXWIDTH, 
     2 * 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:
SURFHANDLE panelTextures[] = { PANEL_STATIC_TEXTURE, panel_dynamic_texture };
And we set the nsurf argument as 2:
PHP:
SetPanelBackground(hPanel, panelTextures, 2, panel_mesh, 
     static_cast<DWORD>(PANEL_WIDTH), static_cast<DWORD>(PANEL_HEIGHT), 
     0, PANEL_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:
// global variables
static const DWORD MFD_COLUMN_NBTS = 6;

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

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

// loadMainPanel(...)
...
for (int i = 0; i < 4 * MFD_COLUMN_NBTS; i++)
{
	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:
void TestAddOn::clbkMFDMode(int mfd, int mode)
{
	if (mfd == MFD_LEFT)
	{
		for (int i = 0; i < 2 * MFD_COLUMN_NBTS; i++)
		{
			change_mfdButton[i] = true;
		}
	}
	else
	{
		for (int i = 2 * MFD_COLUMN_NBTS; i < 4 * MFD_COLUMN_NBTS; i++)
		{
			change_mfdButton[i] = true;
		}
	}
}

Now we can finally start drawing them. We will need a new member function to do that:
PHP:
// global variables
static RECT PANEL_MFD_BUTTON_INACTIVE = { 0, 0, 75, 50 };
static RECT PANEL_MFD_BUTTON_ACTIVE = { 0, 50, 75, 100 };

// class definition
...
void updatePanel()
...
// implementation
void TestAddOn::updatePanel()
{
	for (int y = 0; y < 2; y++)
	{
		for (int x = 0; x < MFD_COLUMN_NBTS * 2; x++)
		{
			int buttonID = y * MFD_COLUMN_NBTS * 2 + x;
			if (change_mfdButton[buttonID])
			{
				change_mfdButton[buttonID] = false;
				RECT tgt = { x * MFD_BUTTON_TEXWIDTH, y * MFD_BUTTON_TEXHEIGHT, (x + 1) * MFD_BUTTON_TEXWIDTH, (y + 1) * MFD_BUTTON_TEXHEIGHT };
				if (buttonID == pressed_mfdButton)
				{
					oapiBlt(panel_dynamic_texture, PANEL_STATIC_TEXTURE, &tgt, &PANEL_MFD_BUTTON_ACTIVE);
				}
				else
				{
					oapiBlt(panel_dynamic_texture, PANEL_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:
if (oapiCockpitMode() == COCKPIT_PANELS)   // only if we're in panel view
{
	updatePanel();
}
The dynamic texture now looks like this:
dynamic-elements.png

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

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:
...
const char* label = oapiMFDButtonLabel((y == 0) ? MFD_LEFT : MFD_RIGHT, x);
If there is no label, this function returns NULL. If we have to display a label, we then retrieve our sketchpad:
PHP:
if (label)
{
	oapi::Sketchpad* sketchpad = 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:
// definition
static oapi::Font* PANEL_FONT_MFD_BUTTON = NULL;

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

// ovcExit(...)
if (PANEL_FONT_MFD_BUTTON) oapiReleaseFont(PANEL_FONT_MFD_BUTTON);
About the face parameter, the documentation says the following:
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:
sketchpad->SetFont(PANEL_FONT_MFD_BUTTON);
sketchpad->SetTextColor(0xFFFFFF);
sketchpad->SetTextAlign(oapi::Sketchpad::CENTER, oapi::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:
sketchpad->Text(static_cast<int>(tgt.left + MFD_BUTTON_TEXWIDTH / 2.0f), static_cast<int>(tgt.top + (tgt.bottom - tgt.top) * 5 / 7.0f), label, strlen(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:
panel-labels.png


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:
// global variables
static const int  PANEL_MFD_LEFT_LEFT_BUTTONS_ID = 0;
static RECT PANEL_MFD_LEFT_LEFT_BUTTONS_RECT = { 2, 60, 47, 340 };
static const int  PANEL_MFD_LEFT_RIGHT_BUTTONS_ID = 1;
static RECT PANEL_MFD_LEFT_RIGHT_BUTTONS_RECT = { 353, 60, 398, 340 };
static const int  PANEL_MFD_RIGHT_LEFT_BUTTONS_ID = 2;
static RECT PANEL_MFD_RIGHT_LEFT_BUTTONS_RECT = { 883, 60, 928, 340 };
static const int  PANEL_MFD_RIGHT_RIGHT_BUTTONS_ID = 3;
static RECT PANEL_MFD_RIGHT_RIGHT_BUTTONS_RECT = { 1232, 60, 1277, 340 };

// loadMainPanel(...)
const RECT nr = { 0, 0, 0, 0 };
RegisterPanelArea(hPanel, PANEL_MFD_LEFT_LEFT_BUTTONS_ID, 
     PANEL_MFD_LEFT_LEFT_BUTTONS_RECT, 2, nr, PANEL_REDRAW_NEVER, 
     PANEL_MOUSE_LBDOWN | PANEL_MOUSE_LBUP | PANEL_MOUSE_LBPRESSED, 
     PANEL_MAP_NONE);
RegisterPanelArea(hPanel, PANEL_MFD_LEFT_RIGHT_BUTTONS_ID, 
     PANEL_MFD_LEFT_RIGHT_BUTTONS_RECT, 2, nr, PANEL_REDRAW_NEVER, 
     PANEL_MOUSE_LBDOWN | PANEL_MOUSE_LBUP | PANEL_MOUSE_LBPRESSED, 
     PANEL_MAP_NONE);
RegisterPanelArea(hPanel, PANEL_MFD_RIGHT_LEFT_BUTTONS_ID, 
     PANEL_MFD_RIGHT_LEFT_BUTTONS_RECT, 2, nr, PANEL_REDRAW_NEVER, 
     PANEL_MOUSE_LBDOWN | PANEL_MOUSE_LBUP | PANEL_MOUSE_LBPRESSED, 
     PANEL_MAP_NONE);
RegisterPanelArea(hPanel, PANEL_MFD_RIGHT_RIGHT_BUTTONS_ID, 
     PANEL_MFD_RIGHT_RIGHT_BUTTONS_RECT, 2, nr, PANEL_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:
// class definition
bool clbkPanelMouseEvent(int id, int event, int mx, int my, void* context);

// implementation
bool TestAddOn::clbkPanelMouseEvent(int id, int event, int mx, int my, void* context)
{
	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:
//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 += 2 * MFD_COLUMN_NBTS;
			break;
		case PANEL_MFD_RIGHT_RIGHT_BUTTONS_ID:
			pressed_mfdButton += 3 * MFD_COLUMN_NBTS;
			break;
		default:
			break;
		}
		change_mfdButton[pressed_mfdButton] = true;
	}
	if (pressed_mfdButton != -1)
	{
		oapiProcessMFDButton((pressed_mfdButton < 2 * MFD_COLUMN_NBTS) ? MFD_LEFT : MFD_RIGHT,
			pressed_mfdButton - ((pressed_mfdButton < 2 * MFD_COLUMN_NBTS) ? 0 : (2 * 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:
Top