Nasal Display Matrix Framework
IMPORTANT: Some, and possibly most, of the features/ideas discussed here are likely to be affected, and possibly even deprecated, by the ongoing work on providing a property tree-based 2D drawing API accessible from Nasal using the new Canvas system available since FlightGear 2.80 (08/2012). Please see: Canvas MCDU Framework for further information
You are advised not to start working on anything directly related to this without first discussing/coordinating your ideas with other FlightGear contributors using the FlightGear developers mailing list or the Canvas subforum . Anything related to Canvas Core Development should be discussed first of all with TheTom and Zakalawe. Nasal-space frameworks are being maintained by Philosopher and Hooray currently. talk page. |
For the development of a Nasal-based CDU a main hurdle was the replication of the screen of the CDU. This Nasal code framework was created to achieve this. Hence, a lot of usage examples can be found in the sources related to the CDU.
The Type of Replicated Display
This framework can replicate any display that uses a fixed row-and-column type screen layout, i.e. a display that has a fixed number of rows (e.g., var ROWS = 14; in cdu_framework.nas) and a fixed number of columns (e.g., var COLS = 24; in cdu_framework.nas), where each of these ROWSxCOLS cells holds exactly one character, resulting in a fixed-spaced font (like most console type fonts).
Furthermore, each cell also holds the formatting information of that cell, i.e. the information on font, color, size, etc.
Overall, such a display can be thought of as a ROWSxCOLSx2 matrix (hence the name of the framework) where the first layer (i.e. all (r,c,1) elements) holds the character information for the cell (r,c) and the the second layer (i.e. all (r,c,2) elements) holds the formatting information.
NOTE: Although generic, the rest of this wiki page will heavily use examples from the CDU - but the concept of the display matrix can be used for other screen like instruments as well.
Nasal Code to Generate Display Matrix Text
The Nasal code for the display matrix is mainly held in display_matrix.nas. Including this fiel into your projects should be sufficient to allow you to use a display matrix.
ScreenText
The key element of making text appear on the 3D screen is the Nasal class ScreenText.
For example, ScreenText-elements make up the label and data lines of fields of the CDU. Several elements can be concatenated in a single line by stacking them in a vector, allowing for a more complex formatting of the text. Here is a brake-down of the Nasal class:
var ScreenText = {
# the actual styles name-to-value hash-map
# skipped, see above for details
# "Static/Public"
func new(t,s=32,ptp=nil),
func registerInPropTree(path),
func sprint(),
func print(),
func update(),
(ScreenText) blank,
# "Private"
scalar me.ptp,
scalar me.style,
scalar me.text,
scalar me.property,
scalar me.size,
func me.mod_property(property_value),
};
Note: The ScreenText.blank does not create a recursion as it is added after the basic hash has been established -- at least this is what I believe... Hcc23 16:23, 10 May 2011 (EDT)
.new - The Constructor
The constructor for screen text is:
new: func(t,s=32,ptp=nil)
The two main elements are text (t) and style (s). Let's look at them in detail:
- s: style is an integer according to the above binary key. In order to make life easier, one can directly use the static hash members, e.g. ScreenText["white"] for small, white text (which is 32, what it defaults to if no other value is given); or ScreenText["GREEN_"] for large, green text with a shaded background.
- t: is the actual (multi-char) text of the element. Some limited constructor overloading can be used in order to generate different effects.
The constructor overlaoding allows for an easy creating of just plain text:
# # 1st method to instantiate a ScreenText # var test_text_verbose = ScreenText.new("Hello World!",ScreenText["WHITE"]); var test_text_shorter = ScreenText.new("Hello World!",34); # does the same as the above as ScreenText["WHITE"]==34
This would create screen text element containing the string Hello World! and format it to be large white font.
However, it could also be used to get a property from the property tree.
# # 2nd method to instantiate a ScreenText # var test_prop_verbose = ScreenText.new(["This is a %s aircraft","/instrumentation/cdu/ident/aircraft"],ScreenText["WHITE"]); var test_prop_shorter = ScreenText.new(["This is a %s aircraft","/instrumentation/cdu/ident/aircraft"],34); # result is identical to above
This last call uses the property /instrumentation/cdu/ident/aircraft in a screen text element. The result is comparable to
var test_text_ = ScreenText.new(sprintf("This is a %s aircraft",getprop("/instrumentation/cdu/ident/aircraft")),ScreenText["WHITE"]);
however, the benefit of the 2nd call method is that the ScreenText internally generates a record of the property (stored in me.property) and as such allows to tie a listener to the screen text, such updating the screen text when the property changes. An example for this can be found in the Nasal CDU Framework.
The third way of instantiating a ScreenText allows a modification of the property via a function:
# # 3rd method to instantiate a ScreenText # var test_modprop_anonymous = ScreenText.new(["This runway is %f football fields long.","/some/where/runway-length-yards",func(prop){return 0.01*prop;}],ScreenText["WHITE"]); var divide_by_100 = func(value) { return 0.01*value;}; var test_modprop_named = ScreenText.new(["This runway is %f football fields long.","/some/where/runway-length-yards",divide_by_100],ScreenText["WHITE"]);
What this does is comparable to this:
ScreenText.new(sprintf("This runway is %f football fields long.",divide_by_100(getprop("/some/where/runway-length-yards"))),ScreenText["WHITE"]);
Again, but with the benefit of registering the function as well as the property and output format in the respective private variables me.mod_property,me.property, and me.format.
Example for a changing the style:
A slight cheat (but a useful one) is incorporated when it comes to altering the style of a ScreenText element. One example for that would be toggle fields in the CDU in where the text essentially stays the same, but it's color (i.e. style) changes depending on a property. The third method of a constructor call can be used to achieve this, here exemplified by one of the before mentioned CDU toggle fields:
var toggle_OnOff_on = func(decision_string) { if (decision_string=="ON") { # "me" is the screen text object that calls this function... me.style = ScreenText["GREEN"]; } else { me.style = ScreenText["white"]; }; return "ON"; }; var toggle_OnOff_off = func(decision_string) { if (decision_string=="OFF") { me.style = ScreenText["GREEN"]; } else { me.style = ScreenText["white"]; }; return "OFF"; }; var field_efisCtl = Field.new(ScreenText.new("EFIS CTL",ScreenText["white"]),[ ScreenText.new(["%s",page_menu_default.ptp~"/data"~"/efis_ctl",toggle_OnOff_off]), ScreenText.new("<>",ScreenText["WHITE"]), ScreenText.new(["%s",page_menu_default.ptp~"/data"~"/efis_ctl",toggle_OnOff_on]), ScreenText.new(">",ScreenText["WHITE"]), ],toggle_efisCtl);
The important part here is the generation of the involved ScreenTexts. Note how the first and third indented ScreenTexts make use of the third form of calling the ScreenText constructor. However, when you check the property modification functions toggle_OnOff_on and toggle_OnOff_off, you will find that they always return the same string ("ON" or "OFF",resp.) which then is rendered in the sprintf() fashion. However, making use of the me Nasal element, both of these functions alter the style via me.style.
.sprint() - Generating a String
.sprint() does exactly that: it takes the associated me screen text, runs it through the formatting process if necessary, and returns a Nasal scalar-type string.
.print() - Print to the Console
This function simply wraps a Nasal print(...) around the .sprint(), effectively printing the screen text into the console. This function might be helpful for debugging purposes.
.update() - Reparsing the ScreenText
This function has no apparent output or return value, but the internal variables, most importantly the .size is updated. This might be necessary when SreenText elements are tied to properties which could result in a non-constant length of the ScreenText string.
Writing the Display Matrix
There are some other functions that actually do the writing into the display matrix, which lives in the property tree. (From there the XML takes it and renders it in the 3D world. But that happens without any necessary interactions.)
resetDisplayMatrix - Erasing
A call to this function via
resetDisplayMatrix();
sets all elements in the display matrix to the ScreenText.blank, i.e. an empty string with a style of "off".
dumpDisplayMatrix - Dump Content to Console
A call to
dumpDisplayMatrix(note="Just testing what happens...");
prints the current display matrix to the console, also putting the optional string note above it, which might be helpful for debugging or identifying which call to dumpDisplayMatrix() actually produced the output.
writeDisplayMatrixText - Print into the Display Matrix
This is the actual print command for the display matrix. The full syntax of the function is
writeDisplayMatrixText(row,col=0,element=nil,mode=nil)
row is the row of the display matrix you want to print to. Rows are zero indexed.
col is the column you want to (start) writing at. At this point the column can mostly be set to/left at its default of 0 as the mode takes care of a lot of things.
element is the vector of ScreenText(s) to be written. Currently the element has to be a vector, even if only one ScreenText is inside it.
mode can be set to "left"/"L", "right"/"R", or "center"/"C" to align the text respectively in the row. Hence, if a mode is set, the col input is currently unused.
One special case of the command is when the element is a nil. In that case the row set by the row input will be completely erased!
Internal Representation of the Data
As the screen has a fixed resolution (governed by the ROWS and COLS variables), internally the (0-indexed) screen is held in the FlightGear property tree. For the CDU the (0,0) cell is stored like this
/instrumentation/cdu/display/row-0/col-0/char # the character printed on the screen /instrumentation/cdu/display/row-0/col-0/style # the style or format of the printed screen.
.../char is a Nasal string-type variable holding the single character that should be displayed, i.e. representing the first layer of the display matrix. .../style is a Nasal scalar-type variable (registering as a double in the property tree although it only requires integers), representing the formatting information, i.e. the second layer of the display matrix.
The initial part of that property tree path, i.e. /instrumentation/cdu is actually held in a variable named PROP (which in case of the CDU is set to the before mentioned value), whereas the display matrix framework adds the ./display/... subtree. This should allow for an easy inclusion of the framework into other instruments as long as that instrument is properly scoped, i.e. it provides its own (locally scoped) var PROP = "/where/ever".
Style Encoding
The style of the char to be displayed is encoded via a binary-style key-map:
# styles binary # wmcgLS -> decimal "off": 0, # 000000 0 # small and LARGE types "white": 32, # 100000 -> 32 # the default style "magenta": 16, # 010000 -> 16 "cyan": 8, # 001000 -> 8 "green": 4, # 000100 -> 4 "WHITE": 34, # 100010 -> 34 "MAGENTA": 18, # 010010 -> 18 "CYAN": 10, # 001010 -> 10 "GREEN": 6, # 000110 -> 6 # shaded small and shaded LARGE types "white_": 33, # 100001 -> 33 "magenta_": 7, # 010001 -> 17 "cyan_": 9, # 001001 -> 9 "green_": 5, # 000101 -> 5 "WHITE_": 35, # 100011 -> 35 "MAGENTA_": 19, # 010011 -> 19 "CYAN_": 11, # 001011 -> 11 "GREEN_": 9, # 000111 -> 9
Each style has a name that is aimed at capturing the features of that style,for example, "white" or "CYAN_". Each name is then mapped to a decimal integer. The integer is defined via a binary key:
- The first group represents the colors: white (w), magenta (m), cyan (c), and green (g).
- The second group represents the font style: large fonts (L) and a shaded background (S).
This concept is translated into the names of the style: it is the name of the color, all lowercase for small fonts, all uppercase for large fonts, and an attached underscore (_) to indicate the selection of a shaded background. So "white" would be the name for a small font size, white font color, non-shaded formatting, whereas "CYAN_" would be standing for a large font size, cyan font color, shaded background formatting.
NOTE: The styles are currently not yet correctly implemented but the framework provides an internal hack to make use of some of the formatting information. Hcc23 09:50, 10 May 2011 (EDT)
The XML Style Interface
In order to make use of the Nasal-maintained data, the 3D model representing the screen has to access the data in the property tree and render it according to the style set there.
This is accomplished via two pieces of code:
- the fairly generic display_matrix_text.xml
- a fairly specific and elaborate section in the model specific XML (in case of the CDU that would be boeing_cdu.xml)
The XML Template for Display Matrix Text
The display_matrix_text.xml essentially provides a template for the creation of an OSGText element, just as other text that is rendered in the the 3D cockpit. The file is comprised of a few sub-sections:
<PropertyList>
<params>
...
</params>
<text>
...
</text>
<animation>
...
</animation>
</PropertyList>
The <params>...</params> section provides a place for all the elements that need to be altered/adapted for the individual characters, i.e. whenever the template is actually used, the values in this section (and if possible this section only) should be changed. More on that later...
The <text>...</text> section provides the actual model for the OSGText. It is a fairly complete set of settings from $FGDATA/Docs/README.osgtext.
The <animation>...</animation> section then alters the rendering of the text, particularly it provides for a material, i.e. the color. This section should hence do the translation from the different styles into the corresponding difference in the rendering. (Which, AFAIK, should be possible through different materials Hcc23 15:15, 10 May 2011 (EDT)).
NOTE: I mentioned earlier that I needed to build in a cheat. Well, it is in the animations section. For some reason I couldn't wrap my head around how to change the color of an animation based upon a variable in the Nasal scope, i.e. a condition like IF A=1 DO.... So I made the display matrix framework put in some extra variables which I did manage to poll, using several animations which get enabled based on mutually exclusive conditions. One drawback (other than it seriously looking lousy) is that I couldn't manage to use that cheat to change the font size. I assume that this is due to the fact that the size is set outside the animation and directly in the text block... So ANY hint on how to generate the text in different sizes (using XML) would be greatly appreciated. Hcc23 15:15, 10 May 2011 (EDT)
The XML Instantiation of Display Matrix Text
So the real work happens to happen inside the CDU's 3D model XML, boeing_cdu.xml. In that file, each an every (r,c) cell of the display matrix gets to be a separate model (yes, that means ton's of XML code...). Here is how one such instance looks like:
<model>
<path>Aircraft/Instruments-3d/cdu/display_matrix_text.xml</path>
<overlay>
<params>
<property type="string">/instrumentation/cdu/display/row-0/col-0/char</property>
<style>/instrumentation/cdu/display/row-0/col-0/style</style>
<font-color>/instrumentation/cdu/display/row-0/col-0/font-color</font-color>
<font-size-m>/instrumentation/cdu/display/row-0/col-0/font-size-m</font-size-m>
</params>
<offsets>
<x-m>0.005</x-m>
<y-m>-0.044</y-m>
<z-m>0.103</z-m>
</offsets>
</overlay>
</model>
What happens here is also two-fold:
- First, the relevant sections in the <params>...</params> block get overwritten.
- Second, the model get's (re-)located at/to the correct position.
NOTE: Here again traces of my cheat are visible. In theory one wouldn't need the font-color and font-size variables, as this information is already encoded in the style variable. However, I couldn't get that to work... Hcc23 15:15, 10 May 2011 (EDT)
As this XML is extremely repetitive, a MATLAB script was used to autocode this section. (The script below will output a file, FILENAME, containing all the XML which then could be copy-pasted into the model's XML.):
fid = fopen(FILENAME,'wb');
width_of_screen = abs(RIGHT-LEFT);
hight_of_screen = abs(TOP-BOTTOM);
delta_col = width_of_screen/COLS;
delta_row = hight_of_screen/ROWS;
x_off = ELEV;
z_off = TOP - 0.5*delta_row;
y_off_zero = LEFT + 0.5*delta_col;
for row = 0:1:ROWS-1
y_off = y_off_zero;
for col = 0:1:COLS-1
charPath = sprintf('/instrumentation/cdu/display/row-%d/col-%d',row,col);
fprintf(fid,[...
'<model>\n',...
'\t<path>%s</path>\n',...
'\t<overlay>\n',...
'\t\t<params>\n',...
'\t\t\t<property type="string">%s</property>\n',...
'\t\t\t<style>%s</style>\n',...
'\t\t\t<font-color>%s</font-color>\n',...
'\t\t\t<font-size-m>%s</font-size-m>\n',...
'\t\t</params>\n',...
'\t\t<offsets>\n',...
'\t\t\t<x-m>%.3f</x-m>\n',...
'\t\t\t<y-m>%.3f</y-m>\n',...
'\t\t\t<z-m>%.3f</z-m>\n',...
'\t\t</offsets>\n',...
'\t</overlay>\n',...
'</model>\n',...
],...
TEMPLATE,...
[charPath '/char'],...
[charPath '/style'],...
[charPath '/font-color'],...
[charPath '/font-size-m'],...
x_off,y_off,z_off);
y_off = y_off + delta_col;
end
z_off = z_off - delta_row;
end
fclose(fid);
For a Nasal-based approach, please check out Howto: Create animation XML files from Nasal.