# Internal helper
var _createColorNodes = func(parent, name)
{
var node = parent.getNode(name, 1);
return [ node.getNode("red", 1),
node.getNode("green", 1),
node.getNode("blue", 1),
node.getNode("alpha", 1) ];
};
var _setColorNodes = func(nodes, color)
{
if( typeof(nodes) != "vector" )
{
debug.warn("This element doesn't support setting color");
return;
}
if( size(color) == 1 )
color = color[0];
if( typeof(color) != "vector" )
return debug.warn("Wrong type for color");
if( size(color) < 3 or size(color) > 4 )
return debug.warn("Color needs 3 or 4 values (RGB or RGBA)");
for(var i = 0; i < size(color); i += 1)
nodes[i].setDoubleValue( color[i] );
if( size(color) == 3 )
# default alpha is 1
nodes[3].setDoubleValue(1);
};
var _getColor = func(color)
{
if( size(color) == 1 )
var color = color[0];
if( typeof(color) == 'scalar' )
return color;
if( typeof(color) != "vector" )
return debug.warn("Wrong type for color");
if( size(color) < 3 or size(color) > 4 )
return debug.warn("Color needs 3 or 4 values (RGB or RGBA)");
var str = 'rgb';
if( size(color) == 4 )
str ~= 'a';
str ~= '(';
# rgb = [0,255], a = [0,1]
for(var i = 0; i < size(color); i += 1)
str ~= (i > 0 ? ',' : '') ~ (i < 3 ? int(color[i] * 255) : color[i]);
return str ~ ')';
};
var _arg2valarray = func
{
var ret = arg;
while ( typeof(ret) == "vector"
and size(ret) == 1 and typeof(ret[0]) == "vector" )
ret = ret[0];
return ret;
}
# Transform
# ==============================================================================
# A transformation matrix which is used to transform an #Element on the canvas.
# The dimensions of the matrix are 3x3 where the last row is always 0 0 1:
#
# a c e
# b d f
# 0 0 1
#
# See http://www.w3.org/TR/SVG/coords.html#TransformMatrixDefined for details.
#
var Transform = {
new: func(node, vals = nil)
{
var m = {
parents: [Transform],
_node: node,
a: node.getNode("m[0]", 1),
b: node.getNode("m[1]", 1),
c: node.getNode("m[2]", 1),
d: node.getNode("m[3]", 1),
e: node.getNode("m[4]", 1),
f: node.getNode("m[5]", 1)
};
var use_vals = typeof(vals) == 'vector' and size(vals) == 6;
# initialize to identity matrix
m.a.setDoubleValue(use_vals ? vals[0] : 1);
m.b.setDoubleValue(use_vals ? vals[1] : 0);
m.c.setDoubleValue(use_vals ? vals[2] : 0);
m.d.setDoubleValue(use_vals ? vals[3] : 1);
m.e.setDoubleValue(use_vals ? vals[4] : 0);
m.f.setDoubleValue(use_vals ? vals[5] : 0);
return m;
},
setTranslation: func
{
var trans = _arg2valarray(arg);
me.e.setDoubleValue(trans[0]);
me.f.setDoubleValue(trans[1]);
return me;
},
# Set rotation (Optionally around a specified point instead of (0,0))
#
# setRotation(rot)
# setRotation(rot, cx, cy)
#
# @note If using with rotation center different to (0,0) don't use
# #setTranslation as it would interfere with the rotation.
setRotation: func(angle)
{
var center = _arg2valarray(arg);
var s = math.sin(angle);
var c = math.cos(angle);
me.a.setDoubleValue(c);
me.b.setDoubleValue(s);
me.c.setDoubleValue(-s);
me.d.setDoubleValue(c);
if( size(center) == 2 )
{
me.e.setDoubleValue( (-center[0] * c) + (center[1] * s) + center[0] );
me.f.setDoubleValue( (-center[0] * s) - (center[1] * c) + center[1] );
}
return me;
},
# Set scale (either as parameters or array)
#
# If only one parameter is given its value is used for both x and y
# setScale(x, y)
# setScale([x, y])
setScale: func
{
var scale = _arg2valarray(arg);
me.a.setDoubleValue(scale[0]);
me.d.setDoubleValue(size(scale) >= 2 ? scale[1] : scale[0]);
return me;
},
getScale: func()
{
# TODO handle rotation
return [me.a.getValue(), me.d.getValue()];
}
};
# Element
# ==============================================================================
# Baseclass for all elements on a canvas
#
var Element = {
# Constructor
#
# @param node Node to be used for element or vector [parent, type] for
# creation of a new node with name type and given parent
# @param id ID/Name (Should be unique)
new: func(node, id)
{
var m = { parents: [PropertyElement.new(node, id), Element] };
m._center = [
m._node.getNode("center[0]"),
m._node.getNode("center[1]")
];
return m;
},
# Trigger an update of the element
#
# Elements are automatically updated once a frame, with a delay of one frame.
# If you wan't to get an element updated in the current frame you have to use
# this method.
update: func()
{
me.setInt("update", 1);
},
# Hide/Show element
#
# @param visible Whether the element should be visible
setVisible: func(visible = 1)
{
me.setBool("visible", visible);
},
getVisible: func me.getBool("visible"),
# Hide element (Shortcut for setVisible(0))
hide: func me.setVisible(0),
# Show element (Shortcut for setVisible(1))
show: func me.setVisible(1),
# Toggle element visibility
toggleVisibility: func me.setVisible( !me.getVisible() ),
#
setGeoPosition: func(lat, lon)
{
me._getTf()._node.getNode("m-geo[4]", 1).setValue("N" ~ lat);
me._getTf()._node.getNode("m-geo[5]", 1).setValue("E" ~ lon);
return me;
},
# Create a new transformation matrix
#
# @param vals Default values (Vector of 6 elements)
createTransform: func(vals = nil)
{
var node = me._node.addChild("tf", 1); # tf[0] is reserved for
# setRotation
return Transform.new(node, vals);
},
# Shortcut for setting translation
setTranslation: func { me._getTf().setTranslation(arg); return me; },
# Set rotation around transformation center (see #setCenter).
#
# @note This replaces the the existing transformation. For additional scale or
# translation use additional transforms (see #createTransform).
setRotation: func(rot)
{
if( me['_tf_rot'] == nil )
# always use the first matrix slot to ensure correct rotation
# around transformation center.
me['_tf_rot'] = Transform.new(me._node.getNode("tf[0]", 1));
me._tf_rot.setRotation(rot, me.getCenter());
return me;
},
# Shortcut for setting scale
setScale: func { me._getTf().setScale(arg); return me; },
# Shortcut for getting scale
getScale: func me._getTf().getScale(),
# Set the fill/background/boundingbox color
#
# @param color Vector of 3 or 4 values in [0, 1]
setColorFill: func me.set('fill', _getColor(arg)),
#
getBoundingBox: func()
{
var bb = me._node.getNode("bounding-box");
if( bb != nil )
{
var min_x = bb.getNode("min-x").getValue();
if( min_x != nil )
return [ min_x,
bb.getNode("min-y").getValue(),
bb.getNode("max-x").getValue(),
bb.getNode("max-y").getValue() ];
}
return [0, 0, 0, 0];
},
# Set transformation center (currently only used for rotation)
setCenter: func()
{
var center = _arg2valarray(arg);
if( size(center) != 2 )
return debug.warn("invalid arg");
if( me._center[0] == nil )
me._center[0] = me._node.getNode("center[0]", 1);
if( me._center[1] == nil )
me._center[1] = me._node.getNode("center[1]", 1);
me._center[0].setDoubleValue(center[0] or 0);
me._center[1].setDoubleValue(center[1] or 0);
return me;
},
# Get transformation center
getCenter: func()
{
var bb = me.getBoundingBox();
var center = [0, 0];
if( me._center[0] != nil )
center[0] = me._center[0].getValue() or 0;
if( me._center[1] != nil )
center[1] = me._center[1].getValue() or 0;
if( bb[0] >= bb[2] or bb[1] >= bb[3] )
return center;
return [ 0.5 * (bb[0] + bb[2]) + center[0],
0.5 * (bb[1] + bb[3]) + center[1] ];
},
# Internal Transform for convenience transform functions
_getTf: func
{
if( me['_tf'] == nil )
me['_tf'] = me.createTransform();
return me._tf;
}
};
# Group
# ==============================================================================
# Class for a group element on a canvas
#
var Group = {
# public:
new: func(node, id)
{
return { parents: [Group, Element.new(node, id)] };
},
# Create a child of given type with specified id.
# type can be group, text
createChild: func(type, id = nil)
{
var factory = me._getFactory(type);
if( factory == nil )
return nil;
return factory([me._node, type], id);
},
# Create multiple children of given type
createChildren: func(type, count)
{
var factory = me._getFactory(type);
if( factory == nil )
return [];
var nodes = me._node.addChildren(type, count, 0, 0);
for(var i = 0; i < count; i += 1)
nodes[i] = factory(nodes[i], nil); # TODO id. Maybe -?
return nodes;
},
# Get a vector of all child elements
getChildren: func()
{
var children = [];
foreach(var c; me._node.getChildren())
if( me._isElementNode(c) )
append(children, me._wrapElement(c));
return children;
},
# Get first child with given id (breadth-first search)
#
# @note Use with care as it can take several miliseconds (for me eg. ~2ms).
getElementById: func(id)
{
# TODO can we improve the queue or better port this to C++ or use some kind
# of lookup hash? Searching is really slow now...
var stack = [me._node];
var index = 0;
while( index < size(stack) )
{
var node = stack[index];
index += 1;
if( node != me._node )
{
var node_id = node.getNode("id");
if( node_id != nil and node_id.getValue() == id )
return me._wrapElement(node);
}
foreach(var c; node.getChildren())
if( me._isElementNode(c) )
append(stack, c);
}
},
# Remove all children
removeAllChildren: func()
{
foreach(var type; keys(me._element_factories))
me._node.removeChildren(type, 0);
return me;
},
# private:
_isElementNode: func(el)
{
# element nodes have type NONE and valid element names (those in the factory
# list)
return el.getType() == "NONE"
and me._element_factories[ el.getName() ] != nil;
},
_wrapElement: func(node)
{
# Create element from existing node
return me._element_factories[ node.getName() ](node, nil);
},
_getFactory: func(type)
{
var factory = me._element_factories[type];
if( factory == nil )
debug.dump("canvas.Group.createChild(): unknown type (" ~ type ~ ")");
return factory;
}
};
# Map
# ==============================================================================
# Class for a group element on a canvas with possibly geopgraphic positions
# which automatically get projected according to the specified projection.
#
var Map = {
new: func(node, id)
{
return { parents: [Map, Group.new(node, id)] };
}
# TODO
};
# Text
# ==============================================================================
# Class for a text element on a canvas
#
var Text = {
new: func(node, id)
{
return { parents: [Text, Element.new(node, id)] };
},
# Set the text
setText: func(text)
{
# add space because osg seems to remove last character if its a space
me.set("text", typeof(text) == 'scalar' ? text ~ ' ' : "");
},
# Set alignment
#
# @param algin String, one of:
# left-top
# left-center
# left-bottom
# center-top
# center-center
# center-bottom
# right-top
# right-center
# right-bottom
# left-baseline
# center-baseline
# right-baseline
# left-bottom-baseline
# center-bottom-baseline
# right-bottom-baseline
#
setAlignment: func(align)
{
me.set("alignment", align);
},
# Set the font size
setFontSize: func(size, aspect = 1)
{
me.setDouble("character-size", size);
me.setDouble("character-aspect-ratio", aspect);
},
# Set font (by name of font file)
setFont: func(name)
{
me.set("font", name);
},
# Enumeration of values for drawing mode:
TEXT: 1, # The text itself
BOUNDINGBOX: 2, # A bounding box (only lines)
FILLEDBOUNDINGBOX: 4, # A filled bounding box
ALIGNMENT: 8, # Draw a marker (cross) at the position of the text
# Set draw mode. Binary combination of the values above. Since I haven't found
# a bitwise or we have to use a + instead.
#
# eg. my_text.setDrawMode(Text.TEXT + Text.BOUNDINGBOX);
setDrawMode: func(mode)
{
me.setInt("draw-mode", mode);
},
# Set bounding box padding
setPadding: func(pad)
{
me.setDouble("padding", pad);
},
setMaxWidth: func(w)
{
me.setDouble("max-width", w);
},
setColor: func me.set('fill', _getColor(arg)),
setColorFill: func me.set('background', _getColor(arg))
};
# Path
# ==============================================================================
# Class for an (OpenVG) path element on a canvas
#
var Path = {
# Path segment commands (VGPathCommand)
VG_CLOSE_PATH: 0,
VG_MOVE_TO: 2,
VG_MOVE_TO_ABS: 2,
VG_MOVE_TO_REL: 3,
VG_LINE_TO: 4,
VG_LINE_TO_ABS: 4,
VG_LINE_TO_REL: 5,
VG_HLINE_TO: 6,
VG_HLINE_TO_ABS: 6,
VG_HLINE_TO_REL: 7,
VG_VLINE_TO: 8,
VG_VLINE_TO_ABS: 8,
VG_VLINE_TO_REL: 9,
VG_QUAD_TO: 10,
VG_QUAD_TO_ABS: 10,
VG_QUAD_TO_REL: 11,
VG_CUBIC_TO: 12,
VG_CUBIC_TO_ABS: 12,
VG_CUBIC_TO_REL: 13,
VG_SQUAD_TO: 14,
VG_SQUAD_TO_ABS: 14,
VG_SQUAD_TO_REL: 15,
VG_SCUBIC_TO: 16,
VG_SCUBIC_TO_ABS: 16,
VG_SCUBIC_TO_REL: 17,
VG_SCCWARC_TO: 20, # Note that CC and CCW commands are swapped. This is
VG_SCCWARC_TO_ABS:20, # needed due to the different coordinate systems used.
VG_SCCWARC_TO_REL:21, # In OpenVG values along the y-axis increase from bottom
VG_SCWARC_TO: 18, # to top, whereas in the Canvas system it is flipped.
VG_SCWARC_TO_ABS: 18,
VG_SCWARC_TO_REL: 19,
VG_LCCWARC_TO: 24,
VG_LCCWARC_TO_ABS:24,
VG_LCCWARC_TO_REL:25,
VG_LCWARC_TO: 22,
VG_LCWARC_TO_ABS: 22,
VG_LCWARC_TO_REL: 23,
# Number of coordinates per command
num_coords: [
0, 0, # VG_CLOSE_PATH
2, 2, # VG_MOVE_TO
2, 2, # VG_LINE_TO
1, 1, # VG_HLINE_TO
1, 1, # VG_VLINE_TO
4, 4, # VG_QUAD_TO
6, 6, # VG_CUBIC_TO
2, 2, # VG_SQUAD_TO
4, 4, # VG_SCUBIC_TO
5, 5, # VG_SCCWARC_TO
5, 5, # VG_SCWARC_TO
5, 5, # VG_LCCWARC_TO
5, 5 # VG_LCWARC_TO
],
#
new: func(node, id)
{
return {
parents: [Path, Element.new(node, id)],
_num_cmds: 0,
_num_coords: 0
};
},
# Remove all existing path data
reset: func
{
me._node.removeChildren('cmd', 0);
me._node.removeChildren('coord', 0);
me._node.removeChildren('coord-geo', 0);
me._num_cmds = 0;
me._num_coords = 0;
return me;
},
# Set the path data (commands and coordinates)
setData: func(cmds, coords)
{
me.reset();
me._node.setValues({cmd: cmds, coord: coords});
me._num_cmds = size(cmds);
me._num_coords = size(coords);
return me;
},
setDataGeo: func(cmds, coords)
{
me.reset();
me._node.setValues({cmd: cmds, 'coord-geo': coords});
me._num_cmds = size(cmds);
me._num_coords = size(coords);
return me;
},
# Add a path segment
addSegment: func(cmd, coords...)
{
var coords = _arg2valarray(coords);
var num_coords = me.num_coords[cmd];
if( size(coords) != num_coords )
debug.warn
(
"Invalid number of arguments (expected " ~ num_coords ~ ")"
);
else
{
me.setInt("cmd[" ~ (me._num_cmds += 1) ~ "]", cmd);
for(var i = 0; i < num_coords; i += 1)
me.setDouble("coord[" ~ (me._num_coords += 1) ~ "]", coords[i]);
}
return me;
},
# Move path cursor
moveTo: func me.addSegment(me.VG_MOVE_TO_ABS, arg),
move: func me.addSegment(me.VG_MOVE_TO_REL, arg),
# Add a line
lineTo: func me.addSegment(me.VG_LINE_TO_ABS, arg),
line: func me.addSegment(me.VG_LINE_TO_REL, arg),
# Add a horizontal line
horizTo: func me.addSegment(me.VG_HLINE_TO_ABS, arg),
horiz: func me.addSegment(me.VG_HLINE_TO_REL, arg),
# Add a vertical line
vertTo: func me.addSegment(me.VG_VLINE_TO_ABS, arg),
vert: func me.addSegment(me.VG_VLINE_TO_REL, arg),
# Add a quadratic Bézier curve
quadTo: func me.addSegment(me.VG_QUAD_TO_ABS, arg),
quad: func me.addSegment(me.VG_QUAD_TO_REL, arg),
# Add a cubic Bézier curve
cubicTo: func me.addSegment(me.VG_CUBIC_TO_ABS, arg),
cubic: func me.addSegment(me.VG_CUBIC_TO_REL, arg),
# Add a smooth quadratic Bézier curve
quadTo: func me.addSegment(me.VG_SQUAD_TO_ABS, arg),
quad: func me.addSegment(me.VG_SQUAD_TO_REL, arg),
# Add a smooth cubic Bézier curve
scubicTo: func me.addSegment(me.VG_SCUBIC_TO_ABS, arg),
scubic: func me.addSegment(me.VG_SCUBIC_TO_REL, arg),
# Draw an elliptical arc (shorter counter-clockwise arc)
arcSmallCCWTo: func me.addSegment(me.VG_SCCWARC_TO_ABS, arg),
arcSmallCCW: func me.addSegment(me.VG_SCCWARC_TO_REL, arg),
# Draw an elliptical arc (shorter clockwise arc)
arcSmallCWTo: func me.addSegment(me.VG_SCWARC_TO_ABS, arg),
arcSmallCW: func me.addSegment(me.VG_SCWARC_TO_REL, arg),
# Draw an elliptical arc (longer counter-clockwise arc)
arcLargeCCWTo: func me.addSegment(me.VG_LCCWARC_TO_ABS, arg),
arcLargeCCW: func me.addSegment(me.VG_LCCWARC_TO_REL, arg),
# Draw an elliptical arc (shorter clockwise arc)
arcLargeCWTo: func me.addSegment(me.VG_LCWARC_TO_ABS, arg),
arcLargeCW: func me.addSegment(me.VG_LCWARC_TO_REL, arg),
# Close the path (implicit lineTo to first point of path)
close: func me.addSegment(me.VG_CLOSE_PATH),
setColor: func me.setStroke(_getColor(arg)),
setColorFill: func me.setFill(_getColor(arg)),
setFill: func(fill)
{
me.set('fill', fill);
},
setStroke: func(stroke)
{
me.set('stroke', stroke);
},
setStrokeLineWidth: func(width)
{
me.setDouble('stroke-width', width);
},
# Set stroke linecap
#
# @param linecap String, "butt", "round" or "square"
#
# See http://www.w3.org/TR/SVG/painting.html#StrokeLinecapProperty for details
setStrokeLineCap: func(linecap)
{
me.set('stroke-linecap', linecap);
},
# Set stroke dasharray
#
# @param pattern Vector, Vector of alternating dash and gap lengths
# [on1, off1, on2, ...]
setStrokeDashArray: func(pattern)
{
if( typeof(pattern) == 'vector' )
me.set('stroke-dasharray', string.join(',', pattern));
else
debug.warn("setStrokeDashArray: vector expected!");
return me;
}
};
# Image
# ==============================================================================
# Class for an image element on a canvas
#
var Image = {
new: func(node, id)
{
var m = {
parents: [Image, Element.new(node, id)]
};
m.color_fill = _createColorNodes(m._node, "color-fill");
return m;
},
# 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("file", file);
},
# Set rectangular region of source image to be used
#
# @param left Rectangle minimum x coordinate
# @param top Rectangle minimum y coordinate
# @param right Rectangle maximum x coordinate
# @param bottom Rectangle maximum y coordinate
# @param normalized Whether to use normalized ([0,1]) or image
# ([0, image_width]/[0, image_height]) coordinates
setSourceRect: func(left, top, right, bottom, normalized = 1)
{
me._node.getNode("source", 1).setValues({
left: left,
top: top,
right: right,
bottom: bottom,
normalized: normalized
});
return me;
},
# Set size of image element
setSize: func(width, height)
{
me._node.setValues({size: [width, height]});
return me;
}
};
# Element factories used by #Group elements to create children
Group._element_factories = {
"group": Group.new,
"map": Map.new,
"text": Text.new,
"path": Path.new,
"image": Image.new
};
# Canvas
# ==============================================================================
# Class for a canvas
#
var Canvas = {
# Place this canvas somewhere onto the object. Pass criterions for placement
# as a hash, eg:
#
# my_canvas.addPlacement({
# "texture": "EICAS.png",
# "node": "PFD-Screen",
# "parent": "Some parent name"
# });
#
# Note that we can choose whichever of the three filter criterions we use for
# matching the target object for our placement. If none of the three fields is
# given every texture of the model will be replaced.
addPlacement: func(vals)
{
var placement = me.texture.addChild("placement", 0, 0);
placement.setValues(vals);
return placement;
},
# Create a new group with the given name
#
# @param id Optional id/name for the group
createGroup: func(id = nil)
{
return Group.new([me.texture, "group"], id);
},
# Set the background color
#
# @param color Vector of 3 or 4 values in [0, 1]
setColorBackground: func { _setColorNodes(me.color, arg); return me; },
# Get path of canvas to be used eg. in Image::setFile
getPath: func()
{
return "canvas://by-index/texture[" ~ me._node.getIndex() ~ "]";
}
};
# Create a new canvas. Pass parameters as hash, eg:
#
# var my_canvas = canvas.new({
# "name": "PFD-Test",
# "size": [512, 512],
# "view": [768, 1024],
# "mipmapping": 1
# });
var new = func(vals)
{
var m = { parents: [Canvas] };
m.texture = Canvas.property_root.addChild("texture", 0, 0);
m.color = _createColorNodes(m.texture, "color-background");
m.texture.setValues(vals);
return m;
};
# Get the first existing canvas with the given name
#
# @param name Name of the canvas
# @return #Canvas, if canvas with #name exists
# nil, otherwise
var get = func(name)
{
var node_canvas = nil;
if( isa(name, props.Node) )
node_canvas = name;
else if( typeof(name) == 'scalar' )
{
foreach(var c; Canvas.property_root.getChildren("texture"))
{
if( c.getValue("name") == name )
node_canvas = c;
}
}
if( node_canvas == nil )
{
debug.warn("Canvas not found: " ~ name);
return nil;
}
return {
parents: [Canvas],
texture: node_canvas,
color: _createColorNodes(node_canvas, "color-background")
};
};
# ------------------------------------------------------------------------------
# Show warnings if API used with too old version of FlightGear without Canvas
# support (Wrapped in anonymous function do not polute the canvas namespace)
(func {
var legacy_dir = getprop("/sim/fg-root") ~ "/Nasal/canvas";
var version_str = getprop("/sim/version/flightgear");
if( string.scanf(version_str, "%u.%u.%u", var fg_version = []) < 1 )
debug.warn("Canvas: Error parsing flightgear version (" ~ version_str ~ ")");
else
{
if( fg_version[0] < 2
or (fg_version[0] == 2 and fg_version[1] < 8) )
{
debug.warn("Canvas: FlightGear version too old (" ~ version_str ~ ")");
gui.popupTip
(
"FlightGear v2.8.0 or newer needed for Canvas support!",
600,
{button: {legend: "Ok", binding: {command: "dialog-close"}}}
);
}
# Load support for older versions of FlightGear (TODO generalize :) )
if( fg_version[0] == 2 and fg_version[1] == 8 )
io.load_nasal(legacy_dir ~ "/api.nas.2.8", "canvas");
}
Canvas.property_root = props.globals.getNode("canvas/by-index", 1);
})();