Howto:Extend the Canvas SVG module

From FlightGear wiki
Jump to navigation Jump to search
This article is a stub. You can help the wiki by expanding it.

Motivation

Increasingly, modern avionics/MFDs are re-created in FlightGear using a combination of vector graphics and raster images - often, raster images also serve as the background image for various parts of a MFD or its modes and "pages". However, for the time being, this must be done manually (in code), because the svg parser used by the Canvas system only supports a tiny subset of SVG tags.

Objective

Document and demonstrate how the Canvas/Nasal SVG module can be extended to suport additional SVG features, by mapping SVG/XML markup to the corresponding Canvas primitives, such as mapping the <image> tag to a Canvas.Image to add support for raster images to the SVG parser.

In the mid-term it would also make sense to add support for animation handling [1], so that such code is encapsulated in the future, which will help us update code if/when native C++ hooks become available to implement animations without any Nasal overhead.

So some tags that may be of interest are:

Background

FlightGear's SVG support is relatively limited - i.e. it's just a tiny subset of SVG that is actually supported by the SVG parser currently, which is then mapped to OpenVG primitives (Canvas.Path).

Often times, it is necessary for people to use only a subset of the features provided by their SVG editor (e.g. Inkscape), or simplify the final result before it can be used in FlightGear by the Canvas system.

Depending on what people have in mind, it may actually be helpful to also learn more about Canvas.Path (see api.nas) and OpenVG in general, because that is how the svg.nas module works behind the scenes when it translates SVG markup to OpenVG paths.

And that's also the easiest way to extend the SVG parser so that it supports additional features, such as for example the image tag.

The SVG standard is rather rich, and many features could be very useful in FlightGear, because the could help greatly reduce the amount of time and expertise required to implement certain features (e.g. loading raster images, supporting line tags or animations in holistic fashion etc).

It would even be possible to come up with some kind of scripting plugin, for e.g. Inkscape, to directly help create/edit SVG files that are intended to represent avionics in FlightGear, e.g. by directly supporting properties[2].

Test Code

Note  The following code is sufficiently self-contained, so that it can be put into a separate Nasal module, but also executed as a standalone snippet using the Nasal Console
var (width,height) = (320,320);
var svg_filename = "/Textures/img.svg";
var title = 'SVGTest: '~svg_filename;

# create a new window, dimensions are WIDTH x HEIGHT, using the dialog decoration (i.e. titlebar)
var window = canvas.Window.new([width,height],"dialog")
 .set('title',title);

# adding a canvas to the new window and setting up background colors/transparency
var myCanvas = window.createCanvas().set("background", canvas.style.getColor("bg_color"));

# creating the top-level/root group which will contain all other elements/group
var root = myCanvas.createGroup();

# change the background color 
myCanvas.set("background", "#ffaac0");

# creating the top-level/root group which will contain all other elements/group
var root = myCanvas.createGroup();

var svg_symbol = root.createChild('group');
canvas.parsesvg(svg_symbol, svg_filename);

Examples

<image> tag

screenshot showing Canvas GUI dialog with SVG image loaded referencing raster images from $FG_ROOT/Textures

Let's assume, we'd like to support raster images, e.g. SVG markup such as the following (for the sake of simplicity, this is simply saved under $FG_ROOT/Textures/img.svg for testing purposes):

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <image x="20" y="20" width="80" height="80" xlink:href="Textures/Splash1.png" />
  <image x="120" y="20" width="80" height="80" xlink:href="Textures/Splash2.png" />
  <image x="220" y="20" width="80" height="80" xlink:href="Textures/Splash3.png" />
  <image x="20" y="120" width="80" height="80" xlink:href="Textures/Splash4.png" />
  <image x="120" y="120" width="80" height="80" xlink:href="Textures/Splash5.png" />
</svg>

This is referencing 5 different splash screen textures found in $FG_ROOT/Textures.

By default, the svg parser will bail out with a message saying Skipping unknown element image. Thus, we need to open fgdata/Nasal/canvas/svg.nas and navigate to the line where that error is triggered:

else
    {
      printlog("info", "Skipping unknown element '" ~ name ~ "'");
      skip = level;
      return;
    }

So, the next step is prepending a new conditional block that looks specifically for the new tag that we'd like to see supported, e.g.:

else if (name == "image")
    {
        printlog("alert", "image tag encountered (not yet implemented)");
    }

Or as a diff/patch:

diff --git a/Nasal/canvas/svg.nas b/Nasal/canvas/svg.nas
index d7fe570..c1b4a90 100644
--- a/Nasal/canvas/svg.nas
+++ b/Nasal/canvas/svg.nas
@@ -578,6 +578,10 @@ var parsesvg = func(group, path, options = nil)
       append(defs_stack, "defs");
       return;
     }
+    else if (name == "image") 
+    {
+       printlog("alert", "image tag encountered (not yet implemented)");
+    }
     else
     {
       printlog("info", "Skipping unknown element '" ~ name ~ "'");

Whenever the parser now sees an image tag, it will tell us so saying: parsesvg: image tag encountered (not yet implemented)

The next step is actually mapping the SVG markup to the corresponding Nasal/Canvas code.

However, for starters, we will keep things simple and add a fixed image from $FG_ROOT/Textures whenever an image tag is encountered, e.g. a splash screen texture.

So, the corresponding Nasal/Canvas magic to load a raster image from the base package looks basically like this:

# create an image child 
var child = root.createChild("image").setFile(URL);

There is another issue here, in that the svg parser is dealing with a stack of operations, so we cannot directly access the root element of the Canvas, but need to push primitives onto the stack, which are applied later on.

To learn how this works, it makes sense to look at existing mappings between SVG and Canvas primitives, such as mappings to Canvas.Text and Canvas.Path (add relevant code snippets below):

pushElement('group', attr['id']);

We also need to take into account that, as per the SVG spec, an image tag may not only refer to raster images, but also other vector graphics (SVG files), which means that we need to look at the file extension - i.e. only add a raster image primitive (Canvas.Image) if the extension is not .svg:

For now, this isn't too useful, as it will only load a fixed texture from the base package, so we need to change things to actually process the proper filename/URL, and other attributes:

debug.dump( attr );

For filename/URL handling, we can look at other xlink/href handling in svg.nas and copy/adapt that accordingly:

var ref = attr["xlink:href"];
      if( ref == nil or size(ref) < 2 or ref[0] != `#` )
        return printlog("warn", "Invalid or missing href: '" ~ ref ~ '"');

Once we do have a valid filename/URL, we can simply set the filename property of the new Canvas.Image child. Once again, it makes sense to look at other elements, e.g. Canvas.Text, and its way to set the value of a node:

 pushElement('text', id);
 stack[-1].set("text", tspan.text);

So, now we need to look up the corresponding property that sets the filename, we can do that by opening api.nas and looking up the setFile() method in Canvas.Image:

# Set image file to be used
#
# @param file Path to file or canvas (Use canvas://... for canvas, eg.
#             canvas://by-index/texture[0])
setFile: func(file)
{
  me.set("src", file);
},

As we can now see easily, the correct property to set is named src.

Thus, we can now come up with a suitable attempt at mapping the SVG image tag to a Canvas.Image:

var ref = attr["xlink:href"];
      if( ref == nil )
        return printlog("warn", "Invalid or missing href in image tag: '" ~ ref ~ '"');
pushElement('image',  attr['id']);
stack[-1].set("src", ref); # set the filename to the xlink/href value

The next step is processing the x,y,width and height attributes of the image tag - to do that, we can also refer to existing code:

diff --git a/Nasal/canvas/svg.nas b/Nasal/canvas/svg.nas
index d7fe570..64e1fbf 100644
--- a/Nasal/canvas/svg.nas
+++ b/Nasal/canvas/svg.nas
@@ -578,6 +578,22 @@ var parsesvg = func(group, path, options = nil)
       append(defs_stack, "defs");
       return;
     }
+    else if (name == "image") 
+    {
+       printlog("alert", "image tag encountered (not yet implemented)");
+
+       var ref = attr["xlink:href"];
+       if( ref == nil )
+               return printlog("alert", "Invalid or missing href in image tag: '" ~ ref ~ '"');
+       pushElement('image',  attr['id']);
+       var image = stack[-1];
+       if ( attr['x'] != nil and attr['y'] != nil)
+               image.setTranslation(attr.x, attr.y);
+       if ( attr['width'] !=nil and attr['height']!=nil)
+               image.setSize(attr.width,attr.height);  
+       image.setFile(ref); # set the filename to the xlink/href value
+       #printlog("alert", "image added");
+    }
     else
     {
       printlog("info", "Skipping unknown element '" ~ name ~ "'");

For now, this is just a proof-of-concept, but it can be easily refined and extended, e.g. to add support for included SVG files, or for animation related tags, or even ECMAScript support via the <script> tag.

<switch> tag

Typically you would use the <switch> element to show different contents based on evaluating a runtime attribute.

The switch element evaluates the requiredFeatures, requiredExtensions and systemLanguage attributes on its direct child elements in order, and then processes and renders the first child for which these attributes evaluate to true. All others will be bypassed and therefore not rendered. If the child element is a container element such as a <g>, then the entire subtree is either processed/rendered or bypassed/not rendered.

In FlightGear, this could for example be used to automatically render a background image or a set of buttons for avionics that are shown in a GUI dialog instead of the 3D cockpit.

Canvas-mfd-framework-prototyping.png