# Internal helper 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 ghost Element ghost as retrieved from core methods new: func(ghost) { return { parents: [PropertyElement, Element, ghost], _node: props.wrapNode(ghost._node_ghost) }; }, # 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]; }, # Calculate the transformation center based on bounding box and center-offset updateCenter: func { me.update(); var bb = me.getTransformedBounds(); if( bb[0] > bb[2] or bb[1] > bb[3] ) return; me._setupCenterNodes ( (bb[0] + bb[2]) / 2 + (me.get("center-offset-x") or 0), (bb[1] + bb[3]) / 2 + (me.get("center-offset-y") or 0) ); return me; }, # Set transformation center (currently only used for rotation) setCenter: func() { var center = _arg2valarray(arg); if( size(center) != 2 ) return debug.warn("invalid arg"); me._setupCenterNodes(center[0], center[1]); return me; }, # Get transformation center getCenter: func() { var center = [0, 0]; me._setupCenterNodes(); 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; return center; }, # Internal Transform for convenience transform functions _getTf: func { if( me['_tf'] == nil ) me['_tf'] = me.createTransform(); return me._tf; }, _setupCenterNodes: func(cx = nil, cy = nil) { if( me["_center"] == nil ) me["_center"] = [ me._node.getNode("center[0]", cx != nil), me._node.getNode("center[1]", cy != nil) ]; if( cx != nil ) me._center[0].setDoubleValue(cx); if( cy != nil ) me._center[1].setDoubleValue(cy); } }; # Group # ============================================================================== # Class for a group element on a canvas # var Group = { # public: new: func(ghost) { return { parents: [Group, Element.new(ghost)] }; }, # Create a child of given type with specified id. # type can be group, text createChild: func(type, id = nil) { var ghost = me._createChild(type, id); var factory = me._getFactory(type); if( factory == nil ) return ghost; return factory(ghost); }, # Create multiple children of given type createChildren: func(type, count) { var factory = me._getFactory(type); if( factory == nil ) return []; var nodes = props._addChildren(me._node._g, [type, count, 0, 0]); for(var i = 0; i < count; i += 1) nodes[i] = factory( me._getChild(nodes[i]) ); return nodes; }, # Create a path child drawing a (rounded) rectangle # # @param x Position of left border # @param y Position of top border # @param w Width # @param h Height # @param cfg Optional settings (eg. {"border-top-radius": 5}) rect: func(x, y, w, h, cfg = nil) { return me.createChild("path").rect(x, y, w, h, cfg); }, # 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). # TODO check with new C++ implementation getElementById: func(id) { var ghost = me._getElementById(id); if( ghost == nil ) return nil; var node = props.wrapNode(ghost._node_ghost); var factory = me._getFactory( node.getName() ); if( factory == nil ) return ghost; return factory(ghost); }, # 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() ]( me._getChild(node._g) ); }, _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(ghost) { return { parents: [Map, Group.new(ghost)] }; } # TODO }; # Text # ============================================================================== # Class for a text element on a canvas # var Text = { new: func(ghost) { return { parents: [Text, Element.new(ghost)] }; }, # Set the text setText: func(text) { 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(ghost) { return { parents: [Path, Element.new(ghost)], _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), # Add a (rounded) rectangle to the path # # @param x Position of left border # @param y Position of top border # @param w Width # @param h Height # @param cfg Optional settings (eg. {"border-top-radius": 5}) rect: func(x, y, w, h, cfg = nil) { var opts = (cfg != nil) ? cfg : {}; # resolve border-[top-,bottom-][left-,right-]radius var br = opts["border-radius"]; if( typeof(br) == 'scalar' ) br = [br, br]; var _parseRadius = func(id) { if( (var r = opts["border-" ~ id ~ "-radius"]) == nil ) { # parse top, bottom, left, right separate if no value specified for # single corner foreach(var s; ["top", "bottom", "left", "right"]) { if( id.starts_with(s ~ "-") ) { r = opts["border-" ~ s ~ "-radius"]; break; } } } if( r == nil ) return br; else if( typeof(r) == 'scalar' ) return [r, r]; else return r; }; # top-left if( (var r = _parseRadius("top-left")) != nil ) { me.moveTo(x, y + r[1]) .arcSmallCWTo(r[0], r[1], 0, x + r[0], y); } else me.moveTo(x, y); # top-right if( (r = _parseRadius("top-right")) != nil ) { me.horizTo(x + w - r[0]) .arcSmallCWTo(r[0], r[1], 0, x + w, y + r[1]); } else me.horizTo(x + w); # bottom-right if( (r = _parseRadius("bottom-right")) != nil ) { me.vertTo(y + h - r[1]) .arcSmallCWTo(r[0], r[1], 0, x + w - r[0], y + h); } else me.vertTo(y + h); # bottom-left if( (r = _parseRadius("bottom-left")) != nil ) { me.horizTo(x + r[0]) .arcSmallCWTo(r[0], r[1], 0, x, y + h - r[1]); } else me.horizTo(x); return me.close(); }, 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(ghost) { return {parents: [Image, Element.new(ghost)]}; }, # 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) { var ghost = me._createGroup(); return { parents: [ Group.new(ghost) ] }; }, # Set the background color # # @param color Vector of 3 or 4 values in [0, 1] setColorBackground: func () { me.texture.getNode('background', 1).setValue(_getColor(arg)); me; }, # Get path of canvas to be used eg. in Image::setFile getPath: func() { return "canvas://by-index/texture[" ~ me.texture.getIndex() ~ "]"; }, # Destructor # # releases associated canvas and makes this object unusable del: func { me.texture.remove(); me.parents = nil; # ensure all ghosts get destroyed } }; var wrapCanvas = func(canvas_ghost) { return { parents: [Canvas, canvas_ghost], texture: props.wrapNode(canvas_ghost._node_ghost) }; } # 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 = wrapCanvas(_newCanvasGhost()); 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(arg) { if( isa(arg, props.Node) ) var node = arg; else if( typeof(arg) == "hash" ) var node = props.Node.new(arg); else die("canvas.new: Invalid argument."); var canvas_ghost = _getCanvasGhost(node._g); if( canvas_ghost == nil ) return nil; return wrapCanvas(canvas_ghost); }; # ------------------------------------------------------------------------------ # 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); })();