]> git.mxchange.org Git - flightgear.git/blobdiff - utils/Modeller/yasim_import.py
Merge branch 'next' of git://gitorious.org/fg/flightgear into next
[flightgear.git] / utils / Modeller / yasim_import.py
index 69b370dd66dcfbae009a9f714aa536c1d369a470..6a862cbd6008254610d10cb76b3aeaf5c6c08d3d 100644 (file)
@@ -9,7 +9,7 @@
 
 __author__ = "Melchior FRANZ < mfranz # aon : at >"
 __url__ = ["http://www.flightgear.org/", "http://cvs.flightgear.org/viewvc/source/utils/Modeller/yasim_import.py"]
-__version__ = "0.1"
+__version__ = "0.2"
 __bpydoc__ = """\
 yasim_import.py loads and visualizes a YASim FDM geometry
 =========================================================
@@ -20,7 +20,7 @@ It is recommended to load the model superimposed over a greyed out and immutable
   (1) load or import aircraft model (menu -> "File" -> "Import" -> "AC3D (.ac) ...")
   (2) create new *empty* scene (menu -> arrow button left of "SCE:scene1" combobox -> "ADD NEW" -> "empty")
   (3) rename scene to yasim (not required)
-  (4) link to scene1 (F10 -> "Output" tab -> arrow button left of text entry "No Set Scene" -> "scene1")
+  (4) link to scene1 (F10 -> "Output" tab in "Buttons Window" -> arrow button left of text entry "No Set Scene" -> "scene1")
   (5) now load the YASim config file (menu -> "File" -> "Import" -> "YASim (.xml) ...")
 
 This is good enough for simple checks. But if you are working on the YASim configuration, then you need a
@@ -58,9 +58,9 @@ Possible variables are:
 
 Of course, absolute FDM coordinates can then no longer directly be read from Blender's 3D view.
 The cursor coordinates display in the script area, however, shows the coordinates in YASim space.
-Note that object names don't contain XML indices but element numbers. YASim_hstab#2 is the third
-hstab in the whole file, not necessarily in its parent XML group. A floating point part in the
-object name (e.g. YASim_hstab#2.004) only means that the geometry has been reloaded that often.
+Note that object names don't contain XML indices but element numbers. YASim_flap0#2 is the third
+flap0 in the whole file, not necessarily in its parent XML group. A floating point part in the
+object name (e.g. YASim_flap0#2.004) only means that the geometry has been reloaded that often.
 It's an unavoidable consequence of how Blender deals with meshes.
 
 
@@ -68,22 +68,40 @@ Elements are displayed as follows:
 
   cockpit                             -> monkey head
   fuselage                            -> blue "tube" (with only 12 sides for less clutter); center at "a"
-  vstab                               -> red with yellow flaps
-  wing/mstab/hstab                    -> green with yellow flaps/spoilers/slats (always 20 cm deep);
-                                         symmetric surfaces are only displayed on the left side
+  vstab                               -> red with yellow control surfaces (flap0, flap1, slat, spoiler)
+  wing/mstab/hstab                    -> green with yellow control surfaces (which are always 20 cm deep);
+                                         symmetric surfaces are only displayed on the left side, unless
+                                         the "Mirror" button is active
   thrusters (jet/propeller/thruster)  -> dashed line from center to actionpt;
                                          arrow from actionpt along thrust vector (always 1 m long);
                                          propeller circle
-  rotor                               -> radius and rel_len_blade_start circle, direction arrow,
-                                         normal and forward vector, one blade at phi0
+  rotor                               -> radius and rel_len_blade_start circle, normal and forward vector,
+                                         one blade at phi0 with direction arrow near blade tip
   gear                                -> contact point and compression vector (no arrow head)
-  tank                                -> cube (10 cm side length)
-  weight                              -> inverted cone
-  ballast                             -> cylinder
-  hitch                               -> circle (10 cm diameter)
+  tank                                -> magenta cube (10 cm side length)
+  weight                              -> inverted cyan cone
+  ballast                             -> yellow cylinder
+  hitch                               -> hexagon (10 cm diameter)
   hook                                -> dashed line for up angle, T-line for down angle
   launchbar                           -> dashed line for up angles, T-line for down angles
+                                         (launchbar and holdback each)
 
+
+The Mirror button complements symmetrical surfaces (wing/hstab/mstab) and control surfaces
+(flap0/flap1/slat/spoiler). This is useful for asymmetrical aircraft, but has the disadvantage
+that it moves the surfaces' object centers from their usual place, yasim's [x, y, z] value,
+to [0, 0, 0]. Turning mirroring off restores the object center.
+
+
+
+Environment variable BLENDER_YASIM_IMPORT can be set to a space-separated list of options:
+
+  $ BLENDER_YASIM_IMPORT="mirror verbose"  blender
+
+whereby:
+
+  verbose  ... enables verbose logs
+  mirror   ... enables mirroring of symmetric surfaces
 """
 
 
@@ -106,11 +124,12 @@ Elements are displayed as follows:
 #--------------------------------------------------------------------------------
 
 
-import Blender, BPyMessages, string, math
+import Blender, BPyMessages, string, math, os
 from Blender.Mathutils import *
 from xml.sax import handler, make_parser
 
 
+CONFIG = string.split(os.getenv("BLENDER_YASIM_IMPORT") or "")
 YASIM_MATRIX = Matrix([-1, 0, 0, 0], [0, -1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1])
 ORIGIN = Vector(0, 0, 0)
 X = Vector(1, 0, 0)
@@ -119,33 +138,35 @@ Z = Vector(0, 0, 1)
 DEG2RAD = math.pi / 180
 RAD2DEG = 180 / math.pi
 
+NO_EVENT = 0
+RELOAD_BUTTON = 1
+CURSOR_BUTTON = 2
+MIRROR_BUTTON = 3
+
+
 
 class Global:
+       verbose = "verbose" in CONFIG
        path = ""
        matrix = None
+       data = None
        cursor = ORIGIN
        last_cursor = Vector(Blender.Window.GetCursorPos())
+       mirror_button = Blender.Draw.Create("mirror" in CONFIG)
+
 
 
 class Abort(Exception):
-       def __init__(self, msg):
+       def __init__(self, msg, term = None):
                self.msg = msg
+               self.term = term
 
 
-def log(msg):
-       #print(msg)     # uncomment to get verbose log messages
-       pass
-
-
-def error(msg):
-       print(("\033[31;1mError: %s\033[m" % msg))
-       Blender.Draw.PupMenu("Error%t|" + msg)
 
+def log(msg):
+       if Global.verbose:
+               print(msg)
 
-def getfloat(attrs, key, default):
-       if attrs.has_key(key):
-               return float(attrs[key])
-       return default
 
 
 def draw_dashed_line(mesh, start, end):
@@ -154,13 +175,14 @@ def draw_dashed_line(mesh, start, end):
        n = len(mesh.verts)
        for i in range(int(1 + 0.5 * (end - start).length / w)):
                a = start + 2 * i * step
-               b = start + (2 * i + 1) * step
+               b = a + step
                if (b - end).length < step.length:
                        b = end
                mesh.verts.extend([a, b])
                mesh.edges.extend([n + 2 * i, n + 2 * i + 1])
 
 
+
 def draw_arrow(mesh, start, end):
        v = end - start
        m = v.toTrackQuat('x', 'z').toMatrix().resize4x4() * TranslationMatrix(start)
@@ -171,6 +193,7 @@ def draw_arrow(mesh, start, end):
        mesh.edges.extend([[n, n + 1], [n + 1, n + 2], [n + 1, n + 3], [n + 4, n + 5]])
 
 
+
 def draw_circle(mesh, numpoints, radius, matrix):
        n = len(mesh.verts)
        for i in range(numpoints):
@@ -182,6 +205,7 @@ def draw_circle(mesh, numpoints, radius, matrix):
                mesh.edges.extend([[n + i, n + i1]])
 
 
+
 class Item:
        scene = Blender.Scene.GetCurrent()
 
@@ -190,13 +214,21 @@ class Item:
                for f in mesh.faces:
                        f.mode |= Blender.Mesh.FaceModes.TWOSIDE | Blender.Mesh.FaceModes.OBCOL
 
-       def set_color(self, mesh, name, color):
-               mat = Blender.Material.New(name)
+       def set_color(self, obj, color):
+               mat = Blender.Material.New()
                mat.setRGBCol(color[0], color[1], color[2])
                mat.setAlpha(color[3])
                mat.mode |= Blender.Material.Modes.ZTRANSP | Blender.Material.Modes.TRANSPSHADOW
+               obj.transp = True
+
+               mesh = obj.getData(mesh = True)
                mesh.materials += [mat]
 
+               for f in mesh.faces:
+                       f.smooth = True
+               mesh.calcNormals()
+
+
 
 class Cockpit(Item):
        def __init__(self, center):
@@ -206,12 +238,15 @@ class Cockpit(Item):
                obj.setMatrix(TranslationMatrix(center) * Global.matrix)
 
 
+
 class Tank(Item):
        def __init__(self, name, center):
                mesh = Blender.Mesh.Primitives.Cube()
                mesh.transform(ScaleMatrix(0.05, 4))
                obj = self.scene.objects.new(mesh, name)
                obj.setMatrix(TranslationMatrix(center) * Global.matrix)
+               self.set_color(obj, [1, 0, 1, 0.5])
+
 
 
 class Ballast(Item):
@@ -220,6 +255,8 @@ class Ballast(Item):
                mesh.transform(ScaleMatrix(0.05, 4))
                obj = self.scene.objects.new(mesh, name)
                obj.setMatrix(TranslationMatrix(center) * Global.matrix)
+               self.set_color(obj, [1, 1, 0, 0.5])
+
 
 
 class Weight(Item):
@@ -228,6 +265,8 @@ class Weight(Item):
                mesh.transform(ScaleMatrix(0.05, 4))
                obj = self.scene.objects.new(mesh, name)
                obj.setMatrix(TranslationMatrix(center) * Global.matrix)
+               self.set_color(obj, [0, 1, 1, 0.5])
+
 
 
 class Gear(Item):
@@ -239,6 +278,7 @@ class Gear(Item):
                obj.setMatrix(TranslationMatrix(center) * Global.matrix)
 
 
+
 class Hook(Item):
        def __init__(self, name, center, length, up_angle, dn_angle):
                mesh = Blender.Mesh.New()
@@ -252,6 +292,7 @@ class Hook(Item):
                obj.setMatrix(TranslationMatrix(center) * Global.matrix)
 
 
+
 class Launchbar(Item):
        def __init__(self, name, lb, lb_length, hb, hb_length, up_angle, dn_angle):
                mesh = Blender.Mesh.New()
@@ -266,13 +307,15 @@ class Launchbar(Item):
                obj.setMatrix(TranslationMatrix(lb) * Global.matrix)
 
 
+
 class Hitch(Item):
        def __init__(self, name, center):
-               mesh = Blender.Mesh.Primitives.Circle(8, 0.1)
+               mesh = Blender.Mesh.Primitives.Circle(6, 0.1)
                obj = self.scene.objects.new(mesh, name)
                obj.setMatrix(RotationMatrix(90, 4, "x") * TranslationMatrix(center) * Global.matrix)
 
 
+
 class Thrust:
        def set_actionpt(self, p):
                self.actionpt = p
@@ -281,6 +324,7 @@ class Thrust:
                self.thrustvector = d
 
 
+
 class Thruster(Thrust, Item):
        def __init__(self, name, center, thrustvector):
                (self.name, self.center, self.actionpt, self.thrustvector) = (name, center, center, thrustvector)
@@ -294,6 +338,7 @@ class Thruster(Thrust, Item):
                obj.setMatrix(TranslationMatrix(self.center) * Global.matrix)
 
 
+
 class Propeller(Thrust, Item):
        def __init__(self, name, center, radius):
                (self.name, self.center, self.radius, self.actionpt, self.thrustvector) = (name, center, radius, center, -X)
@@ -313,6 +358,7 @@ class Propeller(Thrust, Item):
                obj.setMatrix(TranslationMatrix(self.center) * Global.matrix)
 
 
+
 class Jet(Thrust, Item):
        def __init__(self, name, center, rotate):
                (self.name, self.center, self.actionpt) = (name, center, center)
@@ -327,6 +373,7 @@ class Jet(Thrust, Item):
                obj.setMatrix(TranslationMatrix(self.center) * Global.matrix)
 
 
+
 class Fuselage(Item):
        def __init__(self, name, a, b, width, taper, midpoint):
                numvert = 12
@@ -351,10 +398,10 @@ class Fuselage(Item):
                        mesh.faces.extend([[i + numvert, i1 + numvert, i1 + 2 * numvert, i + 2 * numvert]])
 
                mesh.verts.extend([ORIGIN, length * X])
-               self.set_color(mesh, name + "mat", [0, 0, 0.5, 0.4])
                obj = self.scene.objects.new(mesh, name)
-               obj.transp = True
                obj.setMatrix(axis.toTrackQuat('x', 'y').toMatrix().resize4x4() * TranslationMatrix(a) * Global.matrix)
+               self.set_color(obj, [0, 0, 0.5, 0.4])
+
 
 
 class Rotor(Item):
@@ -375,17 +422,18 @@ class Rotor(Item):
                draw_arrow(mesh, ORIGIN, up * invert)
                draw_arrow(mesh, ORIGIN, fwd * invert)
                b += 0.1 * X + direction * chord * Y
-               draw_arrow(mesh, b, b + 0.5 * radius * direction * Y)
+               draw_arrow(mesh, b, b + min(0.5 * radius, 1) * direction * Y)
                obj = self.scene.objects.new(mesh, name)
                obj.setMatrix(matrix * TranslationMatrix(center) * Global.matrix)
 
 
+
 class Wing(Item):
        def __init__(self, name, root, length, chord, incidence, twist, taper, sweep, dihedral):
                #  <1--0--2
                #   \  |  /
                #    4-3-5
-               is_vstab = name.startswith("YASim_vstab")
+               self.is_symmetric = not name.startswith("YASim_vstab#")
                mesh = Blender.Mesh.New()
                mesh.verts.extend([ORIGIN, ORIGIN + 0.5 * chord * X, ORIGIN - 0.5 * chord * X])
                tip = ORIGIN + math.cos(sweep * DEG2RAD) * length * Y - math.sin(sweep * DEG2RAD) * length * X
@@ -394,16 +442,23 @@ class Wing(Item):
                mesh.verts.extend([tip, tipfore, tipaft])
                mesh.faces.extend([[0, 1, 4, 3], [2, 0, 3, 5]])
 
-               self.set_color(mesh, name + "mat", [[0, 0.5, 0, 0.5], [0.5, 0, 0, 0.5]][is_vstab])
                self.make_twosided(mesh)
 
                obj = self.scene.objects.new(mesh, name)
-               obj.transp = True
-               m = Euler(dihedral, -incidence, 0).toMatrix().resize4x4()
-               m *= TranslationMatrix(root)
-               obj.setMatrix(m * Global.matrix)
+               mesh.transform(Euler(dihedral, -incidence, 0).toMatrix().resize4x4())
+               self.set_color(obj, [[0.5, 0.0, 0, 0.5], [0.0, 0.5, 0, 0.5]][self.is_symmetric])
                (self.obj, self.mesh) = (obj, mesh)
 
+               if self.is_symmetric and Global.mirror_button.val:
+                       mod = obj.modifiers.append(Blender.Modifier.Type.MIRROR)
+                       mod[Blender.Modifier.Settings.AXIS_X] = False
+                       mod[Blender.Modifier.Settings.AXIS_Y] = True
+                       mod[Blender.Modifier.Settings.AXIS_Z] = False
+                       mesh.transform(TranslationMatrix(root)) # must move object center to x axis
+                       obj.setMatrix(Global.matrix)
+               else:
+                       obj.setMatrix(TranslationMatrix(root) * Global.matrix)
+
        def add_flap(self, name, start, end):
                a = Vector(self.mesh.verts[2].co)
                b = Vector(self.mesh.verts[5].co)
@@ -416,55 +471,57 @@ class Wing(Item):
                mesh.verts.extend([i0, i1, i0 + c, i1 + c])
                mesh.faces.extend([[0, 1, 3, 2]])
 
-               self.set_color(mesh, name + "mat", [0.8, 0.8, 0, 0.9])
                self.make_twosided(mesh)
 
                obj = self.scene.objects.new(mesh, name)
-               obj.transp = True
                obj.setMatrix(m)
+               self.set_color(obj, [0.8, 0.8, 0, 0.9])
+
+               if self.is_symmetric and Global.mirror_button.val:
+                       mod = obj.modifiers.append(Blender.Modifier.Type.MIRROR)
+                       mod[Blender.Modifier.Settings.AXIS_X] = False
+                       mod[Blender.Modifier.Settings.AXIS_Y] = True
+                       mod[Blender.Modifier.Settings.AXIS_Z] = False
 
 
-class import_yasim(handler.ContentHandler):
+
+class import_yasim(handler.ErrorHandler, handler.ContentHandler):
        ignored = ["cruise", "approach", "control-input", "control-output", "control-speed", \
                        "control-setting", "stall", "airplane", "piston-engine", "turbine-engine", \
                        "rotorgear", "tow", "winch", "solve-weight"]
 
+
        # err_handler
+       def warning(self, exception):
+               print((self.error_string("Warning", exception)))
+
        def error(self, exception):
-               raise Abort(str(exception))
+               print((self.error_string("Error", exception)))
 
        def fatalError(self, exception):
-               raise Abort(str(exception))
+               raise Abort(str(exception), self.error_string("Fatal", exception))
+
+       def error_string(self, tag, e):
+               (column, line) = (e.getColumnNumber(), e.getLineNumber())
+               return "%s: %s\n%s%s^"  % (tag, str(e), Global.data[line - 1], column * ' ')
 
-       def warning(self, exception):
-               print(("WARNING: " + str(exception)))
 
        # doc_handler
-       def setDocumentLocator(self, whatever):
-               pass
+       def setDocumentLocator(self, locator):
+               self.locator = locator
 
        def startDocument(self):
                self.tags = []
                self.counter = {}
                self.items = [None]
-               pass
 
        def endDocument(self):
                for o in Item.scene.objects:
                        o.sel = True
 
-       def characters(self, data):
-               pass
-
-       def ignorableWhitespace(self, data, start, length):
-               pass
-
-       def processingInstruction(self, target, data):
-               pass
-
        def startElement(self, tag, attrs):
                if len(self.tags) == 0 and tag != "airplane":
-                       raise Abort("this isn't a YASim config file")
+                       raise Abort("this isn't a YASim config file (bad root tag at line %d)" % self.locator.getLineNumber())
 
                self.tags.append(tag)
                path = string.join(self.tags, '/')
@@ -485,15 +542,15 @@ class import_yasim(handler.ContentHandler):
                        a = Vector(float(attrs["ax"]), float(attrs["ay"]), float(attrs["az"]))
                        b = Vector(float(attrs["bx"]), float(attrs["by"]), float(attrs["bz"]))
                        width = float(attrs["width"])
-                       taper = getfloat(attrs, "taper", 1)
-                       midpoint = getfloat(attrs, "midpoint", 0.5)
+                       taper = float(attrs.get("taper", 1))
+                       midpoint = float(attrs.get("midpoint", 0.5))
                        log("\033[32mfuselage ax=%f ay=%f az=%f bx=%f by=%f bz=%f width=%f taper=%f midpoint=%f\033[m" % \
                                        (a[0], a[1], a[2], b[0], b[1], b[2], width, taper, midpoint))
                        item = Fuselage("YASim_%s#%d" % (tag, self.counter[tag]), a, b, width, taper, midpoint)
 
                elif tag == "gear":
                        c = Vector(float(attrs["x"]), float(attrs["y"]), float(attrs["z"]))
-                       compression = getfloat(attrs, "compression", 1)
+                       compression = float(attrs.get("compression", 1))
                        up = Z * compression
                        if attrs.has_key("upx"):
                                up = Vector(float(attrs["upx"]), float(attrs["upy"]), float(attrs["upz"])).normalize() * compression
@@ -503,7 +560,7 @@ class import_yasim(handler.ContentHandler):
 
                elif tag == "jet":
                        c = Vector(float(attrs["x"]), float(attrs["y"]), float(attrs["z"]))
-                       rotate = getfloat(attrs, "rotate", 0.0)
+                       rotate = float(attrs.get("rotate", 0))
                        log("\033[36;1mjet x=%f y=%f z=%f rotate=%f\033[m" % (c[0], c[1], c[2], rotate))
                        item = Jet("YASim_jet#%d" % self.counter[tag], c, rotate)
 
@@ -521,7 +578,8 @@ class import_yasim(handler.ContentHandler):
 
                elif tag == "actionpt":
                        if not isinstance(parent, Thrust):
-                               raise Abort("%s is not part of a thruster/propeller/jet" % path)
+                               raise Abort("%s is not part of a thruster/propeller/jet at line %d" \
+                                               % (path, self.locator.getLineNumber()))
 
                        c = Vector(float(attrs["x"]), float(attrs["y"]), float(attrs["z"]))
                        log("\t\033[36mactionpt x=%f y=%f z=%f\033[m" % (c[0], c[1], c[2]))
@@ -529,7 +587,8 @@ class import_yasim(handler.ContentHandler):
 
                elif tag == "dir":
                        if not isinstance(parent, Thrust):
-                               raise Abort("%s is not part of a thruster/propeller/jet" % path)
+                               raise Abort("%s is not part of a thruster/propeller/jet at line %d" \
+                                               % (path, self.locator.getLineNumber()))
 
                        c = Vector(float(attrs["x"]), float(attrs["y"]), float(attrs["z"]))
                        log("\t\033[36mdir x=%f y=%f z=%f\033[m" % (c[0], c[1], c[2]))
@@ -552,9 +611,9 @@ class import_yasim(handler.ContentHandler):
 
                elif tag == "hook":
                        c = Vector(float(attrs["x"]), float(attrs["y"]), float(attrs["z"]))
-                       length = getfloat(attrs, "length", 1.0)
-                       up_angle = getfloat(attrs, "up-angle", 0.0)
-                       down_angle = getfloat(attrs, "down-angle", 70.0)
+                       length = float(attrs.get("length", 1))
+                       up_angle = float(attrs.get("up-angle", 0))
+                       down_angle = float(attrs.get("down-angle", 70))
                        log("\033[35m%s x=%f y=%f z=%f length=%f up-angle=%f down-angle=%f\033[m" \
                                        % (tag, c[0], c[1], c[2], length, up_angle, down_angle))
                        item = Hook("YASim_hook#%d" % self.counter[tag], c, length, up_angle, down_angle)
@@ -566,11 +625,11 @@ class import_yasim(handler.ContentHandler):
 
                elif tag == "launchbar":
                        c = Vector(float(attrs["x"]), float(attrs["y"]), float(attrs["z"]))
-                       length = getfloat(attrs, "length", 1.0)
-                       up_angle = getfloat(attrs, "up-angle", -45.0)
-                       down_angle = getfloat(attrs, "down-angle", 45.0)
-                       holdback = Vector(getfloat(attrs, "holdback-x", c[0]), getfloat(attrs, "holdback-y", c[1]), getfloat(attrs, "holdback-z", c[2]))
-                       holdback_length = getfloat(attrs, "holdback-length", 2.0)
+                       length = float(attrs.get("length", 1))
+                       up_angle = float(attrs.get("up-angle", -45))
+                       down_angle = float(attrs.get("down-angle", 45))
+                       holdback = Vector(float(attrs.get("holdback-x", c[0])), float(attrs.get("holdback-y", c[1])), float(attrs.get("holdback-z", c[2])))
+                       holdback_length = float(attrs.get("holdback-length", 2))
                        log("\033[35m%s x=%f y=%f z=%f length=%f down-angle=%f up-angle=%f holdback-x=%f holdback-y=%f holdback-z+%f holdback-length=%f\033[m" \
                                        % (tag, c[0], c[1], c[2], length, down_angle, up_angle, \
                                        holdback[0], holdback[1], holdback[2], holdback_length))
@@ -580,18 +639,19 @@ class import_yasim(handler.ContentHandler):
                        root = Vector(float(attrs["x"]), float(attrs["y"]), float(attrs["z"]))
                        length = float(attrs["length"])
                        chord = float(attrs["chord"])
-                       incidence = getfloat(attrs, "incidence", 0.0)
-                       twist = getfloat(attrs, "twist", 0.0)
-                       taper = getfloat(attrs, "taper", 1.0)
-                       sweep = getfloat(attrs, "sweep", 0.0)
-                       dihedral = getfloat(attrs, "dihedral", [0.0, 90.0][tag == "vstab"])
+                       incidence = float(attrs.get("incidence", 0))
+                       twist = float(attrs.get("twist", 0))
+                       taper = float(attrs.get("taper", 1))
+                       sweep = float(attrs.get("sweep", 0))
+                       dihedral = float(attrs.get("dihedral", [0, 90][tag == "vstab"]))
                        log("\033[33;1m%s x=%f y=%f z=%f length=%f chord=%f incidence=%f twist=%f taper=%f sweep=%f dihedral=%f\033[m" \
                                        % (tag, root[0], root[1], root[2], length, chord, incidence, twist, taper, sweep, dihedral))
                        item = Wing("YASim_%s#%d" % (tag, self.counter[tag]), root, length, chord, incidence, twist, taper, sweep, dihedral)
 
                elif tag == "flap0" or tag == "flap1" or tag == "slat" or tag == "spoiler":
                        if not isinstance(parent, Wing):
-                               raise Abort("%s is not part of a wing or stab" % path)
+                               raise Abort("%s is not part of a wing or stab at line %d" \
+                                               % (path, self.locator.getLineNumber()))
 
                        start = float(attrs["start"])
                        end = float(attrs["end"])
@@ -599,17 +659,17 @@ class import_yasim(handler.ContentHandler):
                        parent.add_flap("YASim_%s#%d" % (tag, self.counter[tag]), start, end)
 
                elif tag == "rotor":
-                       c = Vector(getfloat(attrs, "x", 0.0), getfloat(attrs, "y", 0.0), getfloat(attrs, "z", 0.0))
-                       norm = Vector(getfloat(attrs, "nx", 0.0), getfloat(attrs, "ny", 0.0), getfloat(attrs, "nz", 1.0))
-                       fwd = Vector(getfloat(attrs, "fx", 1.0), getfloat(attrs, "fy", 0.0), getfloat(attrs, "fz", 0.0))
-                       diameter = getfloat(attrs, "diameter", 10.2)
-                       numblades = int(getfloat(attrs, "numblades", 4))
-                       chord = getfloat(attrs, "chord", 0.3)
-                       twist = getfloat(attrs, "twist", 0.0)
-                       taper = getfloat(attrs, "taper", 1.0)
-                       rel_len_blade_start = getfloat(attrs, "rel-len-blade-start", 0.0)
-                       phi0 = getfloat(attrs, "phi0", 0)
-                       ccw = not not getfloat(attrs, "ccw", 0)
+                       c = Vector(float(attrs.get("x", 0)), float(attrs.get("y", 0)), float(attrs.get("z", 0)))
+                       norm = Vector(float(attrs.get("nx", 0)), float(attrs.get("ny", 0)), float(attrs.get("nz", 1)))
+                       fwd = Vector(float(attrs.get("fx", 1)), float(attrs.get("fy", 0)), float(attrs.get("fz", 0)))
+                       diameter = float(attrs.get("diameter", 10.2))
+                       numblades = int(attrs.get("numblades", 4))
+                       chord = float(attrs.get("chord", 0.3))
+                       twist = float(attrs.get("twist", 0))
+                       taper = float(attrs.get("taper", 1))
+                       rel_len_blade_start = float(attrs.get("rel-len-blade-start", 0))
+                       phi0 = float(attrs.get("phi0", 0))
+                       ccw = not not int(attrs.get("ccw", 0))
 
                        log(("\033[36;1mrotor x=%f y=%f z=%f nx=%f ny=%f nz=%f fx=%f fy=%f fz=%f numblades=%d diameter=%f " \
                                        + "chord=%f twist=%f taper=%f rel_len_blade_start=%f phi0=%f ccw=%d\033[m") \
@@ -628,11 +688,11 @@ class import_yasim(handler.ContentHandler):
                self.items.pop()
 
 
-def extract_matrix(path, tag):
+
+def extract_matrix(filedata, tag):
        v = { 'x': 0.0, 'y': 0.0, 'z': 0.0, 'h': 0.0, 'p': 0.0, 'r': 0.0 }
        has_offsets = False
-       f = open(path)
-       for line in f.readlines():
+       for line in filedata:
                line = string.strip(line)
                if not line.startswith("<!--") or not line.endswith("-->"):
                        continue
@@ -644,23 +704,21 @@ def extract_matrix(path, tag):
                        (key, value) = string.split(assignment, '=', 2)
                        v[string.strip(key)] = float(string.strip(value))
                        has_offsets = True
-       f.close()
-       matrix = None
-       if has_offsets:
-               print(("using offsets: x=%f y=%f z=%f h=%f p=%f r=%f" % (v['x'], v['y'], v['z'], v['h'], v['p'], v['r'])))
-               matrix = Euler(v['r'], v['p'], v['h']).toMatrix().resize4x4()
-               matrix *= TranslationMatrix(Vector(v['x'], v['y'], v['z']))
-       return matrix
+
+       if not has_offsets:
+               return None
+
+       print(("using offsets: x=%f y=%f z=%f h=%f p=%f r=%f" % (v['x'], v['y'], v['z'], v['h'], v['p'], v['r'])))
+       return Euler(v['r'], v['p'], v['h']).toMatrix().resize4x4() * TranslationMatrix(Vector(v['x'], v['y'], v['z']))
 
 
-def run_parser(path):
+
+def load_yasim_config(path):
        if BPyMessages.Error_NoFile(path):
                return
 
-       editmode = Blender.Window.EditMode()
-       if editmode:
-               Blender.Window.EditMode(0)
        Blender.Window.WaitCursor(1)
+       Blender.Window.EditMode(0)
 
        print(("loading '%s'" % path))
        try:
@@ -668,80 +726,99 @@ def run_parser(path):
                        if o.name.startswith("YASim_"):
                                Item.scene.objects.unlink(o)
 
+               f = open(path)
+               Global.data = f.readlines()
+               f.close
+
+               Global.path = path
                Global.matrix = YASIM_MATRIX
-               matrix = extract_matrix(path, "offsets")
+               matrix = extract_matrix(Global.data, "offsets")
                if matrix:
                        Global.matrix *= matrix.invert()
 
-               yasim = make_parser()
-               yasim.setContentHandler(import_yasim())
-               yasim.setErrorHandler(import_yasim())
-               yasim.parse(path)
-
+               Global.yasim.parse(path)
                Blender.Registry.SetKey("FGYASimImportExport", { "path": path }, False)
-               Global.path = path
+               Global.data = None
 
        except Abort, e:
-               print "Error:", e.msg, "  -> aborting ...\n"
+               print(("%s\nAborting ..." % (e.term or e.msg)))
                Blender.Draw.PupMenu("Error%t|" + e.msg)
 
        Blender.Window.RedrawAll()
        Blender.Window.WaitCursor(0)
-       if editmode:
-               Blender.Window.EditMode(1)
 
 
-def draw():
+
+def gui_draw():
        from Blender import BGL, Draw
        (width, height) = Blender.Window.GetAreaSize()
 
        BGL.glClearColor(0.4, 0.4, 0.45, 1)
        BGL.glClear(BGL.GL_COLOR_BUFFER_BIT)
-       Draw.PushButton("Reload YASim", 0, 5, 5, 100, 28)
-       Draw.PushButton("Update Cursor", 1, width - 650, 5, 100, 28)
+
        BGL.glColor3f(1, 1, 1)
+       BGL.glRasterPos2f(5, 55)
+       Draw.Text("FlightGear YASim Import:   '%s'" % Global.path)
 
-       BGL.glRasterPos2f(120, 15)
-       Draw.Text(Global.path)
+       Draw.PushButton("Reload", RELOAD_BUTTON, 5, 5, 80, 32, "reload YASim config file")
+       Global.mirror_button = Draw.Toggle("Mirror", MIRROR_BUTTON, 100, 5, 50, 16, Global.mirror_button.val, \
+                       "show symmetric surfaces on both sides (reloads config)")
+       Draw.PushButton("Update Cursor", CURSOR_BUTTON, width - 650, 5, 100, 32, "update cursor display (in YASim coordinate system)")
 
-       BGL.glRasterPos2f(width - 530 + Blender.Draw.GetStringWidth("Distance from last") - Blender.Draw.GetStringWidth("Current"), 24)
+       BGL.glRasterPos2f(width - 530 + Blender.Draw.GetStringWidth("Vector from last") - Blender.Draw.GetStringWidth("Current"), 24)
        Draw.Text("Current cursor pos:    x = %+.3f    y = %+.3f    z = %+.3f" % tuple(Global.cursor))
 
        c = Global.cursor - Global.last_cursor
        BGL.glRasterPos2f(width - 530, 7)
-       Draw.Text("Distance from last cursor pos:    x = %+.3f    y = %+.3f    z = %+.3f    length = %.3f" % (c[0], c[1], c[2], c.length))
+       Draw.Text("Vector from last cursor pos:    x = %+.3f    y = %+.3f    z = %+.3f    length = %.3f m" % (c[0], c[1], c[2], c.length))
 
 
-def event(ev, value):
+
+def gui_event(ev, value):
        if ev == Blender.Draw.ESCKEY:
                Blender.Draw.Exit()
 
 
-def button(n):
-       if n == 0:
-               run_parser(Global.path)
-       elif n == 1:
+
+def gui_button(n):
+       if n == NO_EVENT:
+               return
+
+       elif n == RELOAD_BUTTON:
+               load_yasim_config(Global.path)
+
+       elif n == CURSOR_BUTTON:
                Global.last_cursor = Global.cursor
                Global.cursor = Vector(Blender.Window.GetCursorPos()) * Global.matrix.invert()
                d = Global.cursor - Global.last_cursor
                print(("cursor:   x=\"%f\" y=\"%f\" z=\"%f\"   dx=%f dy=%f dz=%f   length=%f" \
                                % (Global.cursor[0], Global.cursor[1], Global.cursor[2], d[0], d[1], d[2], d.length)))
+
+       elif n == MIRROR_BUTTON:
+               load_yasim_config(Global.path)
+
        Blender.Draw.Redraw(1)
 
 
+
 def main():
+       log(6 * "\n")
        registry = Blender.Registry.GetKey("FGYASimImportExport", False)
        if registry and "path" in registry and Blender.sys.exists(Blender.sys.expandpath(registry["path"])):
                path = registry["path"]
        else:
                path = ""
 
+       xml_handler = import_yasim()
+       Global.yasim = make_parser()
+       Global.yasim.setContentHandler(xml_handler)
+       Global.yasim.setErrorHandler(xml_handler)
 
-       log(6 * "\n")
        if Blender.Window.GetScreenInfo(Blender.Window.Types.SCRIPT):
-               Blender.Draw.Register(draw, event, button)
+               Blender.Draw.Register(gui_draw, gui_event, gui_button)
+
+       Blender.Window.FileSelector(load_yasim_config, "Import YASim Configuration File", path)
 
-       Blender.Window.FileSelector(run_parser, "Import YASim Configuration File", path)
 
 
 main()