Howto:Parsing 2D Instruments using the Canvas
This article is a stub. You can help the wiki by expanding it. |
The FlightGear forum has a subforum related to: Canvas |
There's also the long-term plan to re-implement our 2D panel manager code in Nasal space through a custom Canvas wrapper - so we could just as well provide a little more than just that stub, I didn't bother looking into supporting the various transformations/actions - but given the flexibility and power of the canvas system, it should actually be fairly straightforward - I cannot currently think of anything "missing" here - the status looks pretty encouraging actually, I think I also remember seeing quite a bit of useful code in Tom's fgdata repo.
Coming up with a Nasal/Canvas wrapper that supports loading our 2D panels should be fairly straightforward, but would involve a fair bit of sophisticated Nasal hacking, i.e. "meta-programming" and familiarity with the canvas system and the 2D panel code obviously
Note All FlightGear disk I/O handled via Nasal scripting and/or fgcommands, is subject to access validation via IOrules. This includes the SGPath bindings in FlightGear 2.99+
However, unlike $FG_ROOT, $FG_HOME is generally accessible for writing, consider this example:
# come up with a path and filename in $FG_HOME, inside the Export sub folder, file name is test.xml
var filename = getprop("/sim/fg-home") ~ "/Export/test.xml";
# use the write_properties() helper in io.nas, which is a wrapper for the savexml fgcommand (see README.commands)
io.write_properties( path: filename, prop: "/sim" );
This will dump the sub branch of the /sim property tree into $FG_HOME/Export/test.xml
For additional examples, see $FG_ROOT/Nasal/io.nas
To learn more about PropertyList processing via loadxml and savexml, please see $FG_ROOT/Docs/README.commands
Internally, all read/write access is validated via an API called fgValidatePath(), for details please see $FG_SRC/Main/util.cxx
Parsing Instruments and Texture Mapping
Here's another stub, that demonstrates how to process existing 2D panel instruments from $FG_AIRCRAFT/Instruments, load them via read_properties() (to be found in io.nas) and then create canvas groups with images for each layer found in the XML file (lots of stuff still missing, such as transformations, bindings/actions). If you'd like to help with this, see Canvas Wrappers#2D Instruments parser
###
# $FG_ROOT/Nasal/parse2dpanel/demo.nas
#
##
# the 2D instrument we are loading textures from
#
var filename = "/Aircraft/Instruments/gyro.xml";
##
# the function that will be called once all files in
# the parse2dpanel folder have been loaded
#
var demo = func {
##
# enable menubar hiding
#
setprop("/sim/menubar/autovisibility/enabled",1);
##
# get the screen coordinates
var x = getprop("/sim/startup/xsize");
var y = getprop("/sim/startup/ysize");
##
# create a new canvas fullscreen window
#
var dlg = canvas.Window.new([x,y]);
##
# set rgba
#
var my_canvas = dlg.createCanvas()
.setColorBackground(1,1,1,1);
##
# create the toplevel canvas group
#
var root = my_canvas.createGroup();
##
# load the PropertyList-encoded XML file
#
var temp=io.read_properties(getprop("/sim/fg-root") ~ filename);
##
# can be used for the z-index attribute, i.e. stacking textures
#
var z =100;
##
# transparently convert the XML file into a Nasal hash using props.nas/getValues()
#
var layers = temp.getValues().layers.layer;
##
# now, go through all layers,
#
foreach(var layer; layers ) {
print("new layer:", layer.name);
# if it's not a texture, skip
if (!contains(layer, "texture")) continue;
# get a handle to the texture of the layer
var texture = layer.texture;
# create an image child for the texture
root.createChild("image")
.setFile( texture.path )
.setSourceRect( texture.x1, texture.x2, texture.y1, texture.y2 )
.setSize(layer.w,layer.h)
.setTranslation(100,100)
.set("z-index", z +=1 );
# TODO: process transformations etc here
} # foreach layer
##
# add a simple event handler, clicking destroys the window
#
my_canvas.addEventListener("click",
func dlg.del()
);
} # end of demo function
##
# register a submodule listener to ensure that our demo function is called
#
_setlistener("/nasal/parse2dpanel/loaded", demo );
Parsing Transformations
Here's basically the same code as before, this time with some boilerplate to help parse the various transformations supported by 2D panel instruments (rotate, x-shift, y-shift), the next step would be populating the placeholders in the Transformation map, so that they actually do something, the transformation callbacks accept a "params" hash, which contains:
- handle to the canvas image group that implements that layer, named params.group
- any optional arguments as members of the params hash, e.g. scale/offset
See $FG_ROOT/Docs/README.xmlpanel for more info about transformations like x-shift, y-shift, rotate and their parameters.
###
# $FG_ROOT/Nasal/parse2dpanel/demo.nas
#
##
# the 2D instrument we are loading textures from
#
var filename = "/Aircraft/Instruments/gyro.xml";
##
# print optional transformation arguments
#
var print_optional = func(hash, key) {
if(contains(hash,key))
print(key,':',hash[key] );
}
##
# show any optional arguments
#
var show_optional = func(hash) {
foreach(var p; ['scale', 'offset', 'property'] )
print_optional(hash, p);
}
##
# show transformation info
#
var transform_info = func(type, params) {
show_optional(params);
}
##
# lookup table/map with transformation names and function callbacks
#
var Transformations = {
'rotation': func(params) {
transform_info("rotation:", params);
},
'x-shift': func(params) {
transform_info("x-shift", params);
},
'y-shift': func(params) {
transform_info("y-shift", params);
},
};
##
# extract optional arguments
#
var extract_optional = func(hash, name, dest) {
if (contains(hash, name))
dest[name] = hash[name];
}
##
# handle a transformation
#
var handleTransform = func(layer, transformation, instrument_layer) {
print("Transform type:", transformation.type);
# wrap all transformation parameters in a hash
var params = {};
# save a handle to the canvas group in the params hash, so that callbacks can access the right sub tree
params.group = instrument_layer;
# now, add transformation-specific args on demand
# debug.dump(transformation);
if (transformation.type == 'rotation')
params.property = transformation.property;
else {
foreach(var opt; ['offset', 'scale'])
extract_optional(transformation, opt, params);
}
##
# register a listener that is invoked once the property changes to update the transformation
if (transformation.type == 'rotation') # dynamic/property transforms
setlistener( transformation.property, func {
Transformations[transformation.type] (params);
}, 1
);
else # apply static transformations immediately
Transformations[transformation.type] (params);
}
##
# the function that will be called once all files in
# the parse2dpanel folder have been loaded
#
var demo = func {
##
# enable menubar hiding
#
setprop("/sim/menubar/autovisibility/enabled",1);
##
# get the screen coordinates
var x = getprop("/sim/startup/xsize");
var y = getprop("/sim/startup/ysize");
##
# create a new canvas fullscreen window
#
var dlg = canvas.Window.new([300,240]);
##
# set rgba
#
var my_canvas = dlg.createCanvas()
.setColorBackground(1,1,1,1);
##
# create the toplevel canvas group
#
var root = my_canvas.createGroup();
##
# load the PropertyList-encoded XML file
#
temp=io.read_properties(getprop("/sim/fg-root") ~ filename);
##
# can be used for the z-index attribute, i.e. stacking textures (layers)
#
var z =100;
##
# create a sub group for this instrument
#
var instrument = root.createChild("group")
.set("instrument-name", temp.getValues().name );
##
# transparently convert the XML file into a Nasal hash using props.nas/getValues()
#
var layers = temp.getValues().layers.layer;
##
# now, go through all layers in the file
#
foreach(var layer; layers ) {
print("new layer:", layer.name);
# if it's not a texture, skip
if (!contains(layer, "texture")) continue;
# get a handle to the texture of the layer
var texture = layer.texture;
# create an image child for the texture
var instrument_layer=instrument.createChild("image")
.set('layer-name', layer.name) # meta information
.setFile( texture.path )
.setSourceRect( texture.x1, texture.x2, texture.y1, texture.y2 )
.setSize(layer.w,layer.h)
.setTranslation(100,100)
.set("z-index", z +=1 );
# TODO: process transformations etc here, for example:
if (contains(layer, "transformations")) {
var transformations = layer.transformations.transformation;
print("Transformations found:", size( transformations ));
if (typeof(transformations)=='vector')
foreach(var t; transformations) {
handleTransform(layer, t, instrument_layer);
} #foreach transformation
} # have transformations
} # foreach layer
##
# add a simple event handler, clicking destroys the window
#
my_canvas.addEventListener("click",
func dlg.del()
);
} # end of demo function
##
# register a submodule listener to ensure that our demo function
# is called once all submodule files are loaded
#
_setlistener("/nasal/parse2dpanel/loaded", demo );
Compass Widget
Inspired by a a recent forum discussion, we thought that creating a compass widget would be a neat little project for someone interested in learning a bit more about Nasal and Canvas coding, and it should be possible to accomplish with less than 50-80 lines of code actually - there's a ton of stuff that you could reuse (SVG compass rose or an existing 2D panel texture), we have quite a few tutorials on Nasal, Canvas and Image processing (check the wiki). Furthermore, we have the long-term plan to eventually phase out the old 2D panel code and used a modernized Canvas-based version to help with Unifying the 2D rendering backend via canvas.
Paste this into your Nasal Console and click Execute:
var CanvasApplication = {
##
# constructor
new: func(x=300,y=200) {
var m = { parents: [CanvasApplication] };
m.dlg = canvas.Window.new([x,y],"dialog");
m.canvas = m.dlg.createCanvas().setColorBackground(1,1,1,0.5);
m.root = m.canvas.createGroup();
m.timer = maketimer(0.1, func m.update() );
m.init();
return m;
},
del: func me.timer.stop(),
update: func() {
var hdg=getprop("/orientation/heading-deg");
me.compass.setRotation(-hdg*D2R);
},
init: func() {
var filename = "/Aircraft/Instruments/gyro.xml";
var temp= io.read_properties(getprop("/sim/fg-root") ~ filename);
var layers = temp.getValues().layers.layer;
var z=100;
foreach(var layer; layers ) {
print("new layer:", layer.name);
# if it's not a texture, skip
if (!contains(layer, "texture")) continue;
# get a handle to the texture of the layer
var texture = layer.texture;
# create an image child for the texture
var child=me.root.createChild("image")
.setFile( texture.path )
.setSourceRect( texture.x1, texture.x2, texture.y1, texture.y2 )
.setSize(layer.w,layer.h)
.setTranslation(20,20)
.set("z-index", z +=1 )
.setScale(2.5);
if (layer.w != nil and layer.h!=nil)
child.setCenter(layer.w/2, layer.h/2);
if (layer.name=="compass rose") {
print("Found compass layer");
# child.setCenter(55,55);
me.compass = child;
}
} # foreach
me.timer.start();
},
}; # end of CanvasApplication
var InstrumentWidget = {
new: func(x,y) {
var m = CanvasApplication.new(x:x,y:y);
},
};
var compass = InstrumentWidget.new(x:300, y:300);
print("Compass Loaded...!");