Added Thrust Torque readout to Editor and Flight
Added Thrust Torque readout to Editor and Flight

Shows the torque experienced by the ship at full throttle due to asymmetries in its build.
An other readout shows a pseudo-angle that approximates the directional error of the engine's thrust vectors (more human-readable than the kNm Torque number).

--- a/Documents/CHANGES.txt
+++ b/Documents/CHANGES.txt
@@ -1,4 +1,22 @@
-1.0.11.0
+1.0.11.3, 11-11-2014
+    Changed: Gravity measurements for Isp to 9.82.
+
+1.0.11.2, 10-11-2014
+    Changed: Gravity measurements for Isp calculations from 9.81 to 9.8066 for accuracy.
+    Changed: Manoeuvre node burn times are now more accurate.
+    Fixed: Bug in the manoeuvre node burn time calculations where it was not averaging acceleration correctly.
+
+1.0.11.1, 07-11-2014
+    Changed: Build Engineer now shows stage part count as well as total.
+    Changed: Build Overlay Vessel tab data:
+        DeltaV: stage / total
+        Mass:   stage / total
+        TWR:    start (max)   <- shows for bottom stage only.
+        Parts:  stage / total
+
+    Fixed: Issue with the vessel tab vanishing from the editor.
+
+1.0.11.0, 06-11-2014
     Added: New readouts to the orbital category:
         - Current SOI
         - Manoeuvre Node DeltaV (Prograde)

--- a/KerbalEngineer/Editor/BuildAdvanced.cs
+++ b/KerbalEngineer/Editor/BuildAdvanced.cs
@@ -451,7 +451,7 @@
             {
                 if (this.showAllStages || stage.deltaV > 0)
                 {
-                    GUILayout.Label(stage.partCount.ToString("N0"), this.infoStyle);
+                    GUILayout.Label(stage.partCount + " / " + stage.totalPartCount, this.infoStyle);
                 }
             }
             GUILayout.EndVertical();
@@ -528,6 +528,23 @@
                 if (this.showAllStages || stage.deltaV > 0)
                 {
                     GUILayout.Label(stage.thrust.ToForce(), this.infoStyle);
+                }
+            }
+            GUILayout.EndVertical();
+        }
+
+        /// <summary>
+        ///     Draws the torque column.
+        /// </summary>
+        private void DrawTorque()
+        {
+            GUILayout.BeginVertical(GUILayout.Width(75.0f * GuiDisplaySize.Offset));
+            GUILayout.Label("TORQUE", this.titleStyle);
+            foreach (var stage in this.stages)
+            {
+                if (this.showAllStages || stage.deltaV > 0)
+                {
+                    GUILayout.Label(stage.maxThrustTorque.ToTorque(), this.infoStyle);
                 }
             }
             GUILayout.EndVertical();
@@ -738,6 +755,7 @@
                     this.DrawMass();
                     this.DrawIsp();
                     this.DrawThrust();
+                    this.DrawTorque();
                     this.DrawTwr();
                     this.DrawDeltaV();
                     this.DrawBurnTime();

--- a/KerbalEngineer/Editor/BuildOverlayResources.cs
+++ b/KerbalEngineer/Editor/BuildOverlayResources.cs
@@ -70,7 +70,7 @@
         {
             try
             {
-                if (!BuildOverlay.Visible || this.resources.Count == 0 || EditorLogic.fetch.editorScreen != EditorLogic.EditorScreen.Parts)
+                if (!Visible || this.resources.Count == 0 || EditorLogic.fetch.editorScreen != EditorLogic.EditorScreen.Parts)
                 {
                     return;
                 }
@@ -104,7 +104,7 @@
         {
             try
             {
-                if (!BuildOverlay.Visible)
+                if (!Visible)
                 {
                     return;
                 }

--- a/KerbalEngineer/Editor/BuildOverlayVessel.cs
+++ b/KerbalEngineer/Editor/BuildOverlayVessel.cs
@@ -22,7 +22,7 @@
 using System;
 using System.Collections.Generic;
 
-using KerbalEngineer.Extensions;
+using KerbalEngineer.Helpers;
 using KerbalEngineer.VesselSimulator;
 
 using UnityEngine;
@@ -33,6 +33,12 @@
 {
     public class BuildOverlayVessel : MonoBehaviour
     {
+        #region Constants
+
+        private const float Width = 175.0f;
+
+        #endregion
+
         #region Fields
 
         private static bool visible = true;
@@ -45,7 +51,7 @@
         private GUIContent tabContent;
         private Rect tabPosition;
         private Vector2 tabSize;
-        private Rect windowPosition = new Rect(300.0f, 0.0f, BuildOverlay.MinimumWidth, 0.0f);
+        private Rect windowPosition = new Rect(300.0f, 0.0f, Width, 0.0f);
 
         #endregion
 
@@ -76,7 +82,7 @@
         {
             try
             {
-                if (!Visible || EditorLogic.startPod == null || this.lastStage == null || EditorLogic.fetch.editorScreen != EditorLogic.EditorScreen.Parts)
+                if (!Visible || EditorLogic.startPod == null || EditorLogic.fetch.editorScreen != EditorLogic.EditorScreen.Parts)
                 {
                     return;
                 }
@@ -110,7 +116,7 @@
         {
             try
             {
-                if (!BuildOverlay.Visible || EditorLogic.startPod == null)
+                if (!Visible || EditorLogic.startPod == null)
                 {
                     return;
                 }
@@ -144,6 +150,10 @@
             }
 
             this.windowPosition.y = Mathf.Lerp(Screen.height, Screen.height - this.windowPosition.height, this.openPercent);
+            if (this.windowPosition.width < Width)
+            {
+                this.windowPosition.width = Width;
+            }
             this.tabPosition.width = this.tabSize.x;
             this.tabPosition.height = this.tabSize.y;
             this.tabPosition.x = this.windowPosition.x;
@@ -174,10 +184,10 @@
             if (this.lastStage != null)
             {
                 this.infoItems.Clear();
-                this.infoItems.Add(new PartInfoItem("Delta-V", this.lastStage.totalDeltaV.ToString("N0") + "m/s"));
-                this.infoItems.Add(new PartInfoItem("Mass", this.lastStage.totalMass.ToMass()));
-                this.infoItems.Add(new PartInfoItem("TWR", this.lastStage.thrustToWeight.ToString("F2")));
-                this.infoItems.Add(new PartInfoItem("Parts", this.lastStage.partCount.ToString("N0")));
+                this.infoItems.Add(new PartInfoItem("Delta-V", this.lastStage.deltaV.ToString("N0") + " / " + this.lastStage.totalDeltaV.ToString("N0") + "m/s"));
+                this.infoItems.Add(new PartInfoItem("Mass", Units.ToMass(this.lastStage.mass, this.lastStage.totalMass)));
+                this.infoItems.Add(new PartInfoItem("TWR", this.lastStage.thrustToWeight.ToString("F2") + " (" + this.lastStage.maxThrustToWeight.ToString("F2") + ")"));
+                this.infoItems.Add(new PartInfoItem("Parts", this.lastStage.partCount + " / " + this.lastStage.totalPartCount));
             }
         }
 
@@ -198,7 +208,7 @@
                     if (item.Value != null)
                     {
                         GUILayout.Label(item.Name + ":", BuildOverlay.NameStyle);
-                        GUILayout.Space(50.0f);
+                        GUILayout.FlexibleSpace();
                         GUILayout.Label(item.Value, BuildOverlay.ValueStyle);
                     }
                     else

--- a/KerbalEngineer/EngineerGlobals.cs
+++ b/KerbalEngineer/EngineerGlobals.cs
@@ -33,7 +33,7 @@
         /// <summary>
         ///     Current version of the Kerbal Engineer assembly.
         /// </summary>
-        public const string AssemblyVersion = "1.0.11";
+        public const string AssemblyVersion = "1.0.11.3";
 
         #endregion
 

--- a/KerbalEngineer/Extensions/DoubleExtensions.cs
+++ b/KerbalEngineer/Extensions/DoubleExtensions.cs
@@ -29,6 +29,11 @@
     {
         #region Methods: public
 
+        public static double Clamp(this double value, double lower, double higher)
+        {
+            return value < lower ? lower : value > higher ? higher : value;
+        }
+
         public static string ToAcceleration(this double value)
         {
             return Units.ToAcceleration(value);
@@ -42,6 +47,11 @@
         public static string ToDistance(this double value)
         {
             return Units.ToDistance(value);
+        }
+
+        public static string ToTorque(this double value)
+        {
+            return Units.ToTorque(value);
         }
 
         public static string ToForce(this double value)

--- a/KerbalEngineer/Flight/Readouts/Orbital/ManoeuvreNode/ManoeuvreProcessor.cs
+++ b/KerbalEngineer/Flight/Readouts/Orbital/ManoeuvreNode/ManoeuvreProcessor.cs
@@ -47,6 +47,7 @@
         public static double AvailableDeltaV { get; private set; }
 
         public static double BurnTime { get; private set; }
+
         public static int FinalStage { get; private set; }
 
         public static double HalfBurnTime { get; private set; }
@@ -110,7 +111,7 @@
 
             var burnTime = 0.0;
             var midPointTime = 0.0;
-            HasDeltaV = GetBurnTime((float)TotalDeltaV, ref burnTime, ref midPointTime);
+            HasDeltaV = GetBurnTime(TotalDeltaV, ref burnTime, ref midPointTime);
             AvailableDeltaV = SimulationProcessor.LastStage.totalDeltaV;
 
             BurnTime = burnTime;
@@ -123,47 +124,50 @@
 
         #region Methods: private
 
-        private static bool GetBurnTime(float deltaV, ref double burnTime, ref double midPointTime)
+        private static bool GetBurnTime(double deltaV, ref double burnTime, ref double midPointTime)
         {
             var setMidPoint = false;
-            var deltaVMidPoint = deltaV * 0.5f;
+            var deltaVMidPoint = deltaV * 0.5;
 
-            for (var i = SimulationProcessor.Stages.Length - 1; i >= 0; i--)
+            for (var i = SimulationProcessor.Stages.Length - 1; i > -1; i--)
             {
                 var stage = SimulationProcessor.Stages[i];
-                var stageDeltaV = (float)stage.deltaV;
+                var stageDeltaV = stage.deltaV;
+                var startMass = stage.totalMass;
 
                 ProcessStageDrain:
-                if (deltaV <= Single.Epsilon)
+                if (deltaV <= Double.Epsilon)
                 {
-                    FinalStage = ++i;
-                    return true;
+                    break;
                 }
-                if (stageDeltaV <= Single.Epsilon)
+                if (stageDeltaV <= Double.Epsilon)
                 {
                     continue;
                 }
 
-                float deltaVDrain;
+                FinalStage = i;
+
+                double deltaVDrain;
                 if (deltaVMidPoint > 0.0)
                 {
-                    deltaVDrain = Mathf.Clamp(deltaV, 0.0f, Mathf.Clamp(deltaVMidPoint, 0.0f, stageDeltaV));
+                    deltaVDrain = deltaV.Clamp(0.0, stageDeltaV.Clamp(0.0, deltaVMidPoint));
                     deltaVMidPoint -= deltaVDrain;
-                    setMidPoint = deltaVMidPoint <= Single.Epsilon;
+                    setMidPoint = deltaVMidPoint <= Double.Epsilon;
                 }
                 else
                 {
-                    deltaVDrain = Mathf.Clamp(deltaV, 0.0f, stageDeltaV);
+                    deltaVDrain = deltaV.Clamp(0.0, stageDeltaV);
                 }
 
-                var startMass = stage.totalMass - (stage.resourceMass * (1.0f - (stageDeltaV / stage.deltaV)));
-                var endMass = startMass - (stage.resourceMass * (deltaVDrain / stageDeltaV));
-                var minimumAcceleration = stage.thrust / startMass;
-                var maximumAcceleration = stage.thrust / endMass;
+                var exhaustVelocity = stage.isp * 9.82;
+                var flowRate = stage.thrust / exhaustVelocity;
+                var endMass = Math.Exp(Math.Log(startMass) - deltaVDrain / exhaustVelocity);
+                var deltaMass = (startMass - endMass) * Math.Exp(-(deltaVDrain * 0.001) / exhaustVelocity);
+                burnTime += deltaMass / flowRate;
 
-                burnTime += deltaVDrain / ((minimumAcceleration + maximumAcceleration) * 0.5);
                 deltaV -= deltaVDrain;
                 stageDeltaV -= deltaVDrain;
+                startMass -= deltaMass;
 
                 if (setMidPoint)
                 {
@@ -172,7 +176,7 @@
                     goto ProcessStageDrain;
                 }
             }
-            return false;
+            return deltaV <= Double.Epsilon;
         }
 
         #endregion

--- a/KerbalEngineer/Flight/Readouts/ReadoutLibrary.cs
+++ b/KerbalEngineer/Flight/Readouts/ReadoutLibrary.cs
@@ -132,6 +132,8 @@
                 readouts.Add(new Mass());
                 readouts.Add(new Thrust());
                 readouts.Add(new ThrustToWeight());
+                readouts.Add(new ThrustOffsetAngle());
+                readouts.Add(new ThrustTorque());
                 readouts.Add(new SurfaceThrustToWeight());
                 readouts.Add(new Acceleration());
                 readouts.Add(new SuicideBurnAltitude());

--- /dev/null
+++ b/KerbalEngineer/Flight/Readouts/Vessel/ThrustOffsetAngle.cs
@@ -1,1 +1,66 @@
+// 
+//     Kerbal Engineer Redux
+// 
+//     Copyright (C) 2014 CYBUTEK
+// 
+//     This program is free software: you can redistribute it and/or modify
+//     it under the terms of the GNU General Public License as published by
+//     the Free Software Foundation, either version 3 of the License, or
+//     (at your option) any later version.
+// 
+//     This program is distributed in the hope that it will be useful,
+//     but WITHOUT ANY WARRANTY; without even the implied warranty of
+//     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//     GNU General Public License for more details.
+// 
+//     You should have received a copy of the GNU General Public License
+//     along with this program.  If not, see <http://www.gnu.org/licenses/>.
+// 
 
+#region Using Directives
+
+using KerbalEngineer.Flight.Sections;
+using KerbalEngineer.Helpers;
+
+#endregion
+
+namespace KerbalEngineer.Flight.Readouts.Vessel
+{
+    public class ThrustOffsetAngle : ReadoutModule
+    {
+        #region Constructors
+
+        public ThrustOffsetAngle()
+        {
+            this.Name = "Thrust offset angle";
+            this.Category = ReadoutCategory.GetCategory("Vessel");
+            this.HelpString = "Thrust angle offset due to vessel asymmetries and gimballing";
+            this.IsDefault = true;
+        }
+
+        #endregion
+
+        #region Methods: public
+
+        public override void Draw(SectionModule section)
+        {
+            if (SimulationProcessor.ShowDetails)
+            {
+                this.DrawLine(Units.ToAngle(SimulationProcessor.LastStage.thrustOffsetAngle, 1), section.IsHud);
+            }
+        }
+
+        public override void Reset()
+        {
+            FlightEngineerCore.Instance.AddUpdatable(SimulationProcessor.Instance);
+        }
+
+        public override void Update()
+        {
+            SimulationProcessor.RequestUpdate();
+        }
+
+        #endregion
+    }
+}
+

--- /dev/null
+++ b/KerbalEngineer/Flight/Readouts/Vessel/ThrustTorque.cs
@@ -1,1 +1,66 @@
+// 
+//     Kerbal Engineer Redux
+// 
+//     Copyright (C) 2014 CYBUTEK
+// 
+//     This program is free software: you can redistribute it and/or modify
+//     it under the terms of the GNU General Public License as published by
+//     the Free Software Foundation, either version 3 of the License, or
+//     (at your option) any later version.
+// 
+//     This program is distributed in the hope that it will be useful,
+//     but WITHOUT ANY WARRANTY; without even the implied warranty of
+//     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//     GNU General Public License for more details.
+// 
+//     You should have received a copy of the GNU General Public License
+//     along with this program.  If not, see <http://www.gnu.org/licenses/>.
+// 
 
+#region Using Directives
+
+using KerbalEngineer.Flight.Sections;
+using KerbalEngineer.Helpers;
+
+#endregion
+
+namespace KerbalEngineer.Flight.Readouts.Vessel
+{
+    public class ThrustTorque : ReadoutModule
+    {
+        #region Constructors
+
+        public ThrustTorque()
+        {
+            this.Name = "Thrust torque";
+            this.Category = ReadoutCategory.GetCategory("Vessel");
+            this.HelpString = "Thrust torque due to vessel asymmetries and gimballing";
+            this.IsDefault = true;
+        }
+
+        #endregion
+
+        #region Methods: public
+
+        public override void Draw(SectionModule section)
+        {
+            if (SimulationProcessor.ShowDetails)
+            {
+                this.DrawLine(Units.ToTorque(SimulationProcessor.LastStage.maxThrustTorque), section.IsHud);
+            }
+        }
+
+        public override void Reset()
+        {
+            FlightEngineerCore.Instance.AddUpdatable(SimulationProcessor.Instance);
+        }
+
+        public override void Update()
+        {
+            SimulationProcessor.RequestUpdate();
+        }
+
+        #endregion
+    }
+}
+

--- /dev/null
+++ b/KerbalEngineer/Helpers/Averager.cs
@@ -1,1 +1,67 @@
+// 
+//     Kerbal Engineer Redux
+// 
+//     Copyright (C) 2014 CYBUTEK
+// 
+//     This program is free software: you can redistribute it and/or modify
+//     it under the terms of the GNU General Public License as published by
+//     the Free Software Foundation, either version 3 of the License, or
+//     (at your option) any later version.
+// 
+//     This program is distributed in the hope that it will be useful,
+//     but WITHOUT ANY WARRANTY; without even the implied warranty of
+//     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//     GNU General Public License for more details.
+// 
+//     You should have received a copy of the GNU General Public License
+//     along with this program.  If not, see <http://www.gnu.org/licenses/>.
+// 
 
+using System;
+
+namespace KerbalEngineer
+{ 
+    public class VectorAverager
+    {
+        private Vector3d sum = Vector3d.zero;
+        private uint count = 0;
+
+        public void Add(Vector3d v) {
+            sum += v;
+            count += 1;
+        }
+
+        public Vector3d Get() {
+            if (count > 0) {
+                return sum / count;
+            } else {
+                return Vector3d.zero;
+            }
+        }
+    }
+
+    public class WeightedVectorAverager
+    {
+        private Vector3d sum = Vector3d.zero;
+        private double totalweight = 0;
+
+        public void Add(Vector3d v, double weight) {
+            sum += v * weight;
+            totalweight += weight;
+        }
+
+        public Vector3d Get() {
+            if (totalweight > 0) {
+                return sum / totalweight;
+            } else {
+                return Vector3d.zero;
+            }
+        }
+
+        public double GetTotalWeight() {
+            return totalweight;
+        }
+    }
+}
+
+

--- /dev/null
+++ b/KerbalEngineer/Helpers/ForceAccumulator.cs
@@ -1,1 +1,103 @@
+// 
+//     Kerbal Engineer Redux
+// 
+//     Copyright (C) 2014 CYBUTEK
+// 
+//     This program is free software: you can redistribute it and/or modify
+//     it under the terms of the GNU General Public License as published by
+//     the Free Software Foundation, either version 3 of the License, or
+//     (at your option) any later version.
+// 
+//     This program is distributed in the hope that it will be useful,
+//     but WITHOUT ANY WARRANTY; without even the implied warranty of
+//     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//     GNU General Public License for more details.
+// 
+//     You should have received a copy of the GNU General Public License
+//     along with this program.  If not, see <http://www.gnu.org/licenses/>.
+// 
 
+using System;
+using System.Collections.Generic;
+
+namespace KerbalEngineer
+{
+    // a (force, application point) tuple
+    public class AppliedForce
+    {
+        public Vector3d vector;
+        public Vector3d applicationPoint;
+
+        public AppliedForce(Vector3d vector, Vector3d applicationPoint) {
+            this.vector = vector;
+            this.applicationPoint = applicationPoint;
+        }
+    }
+
+	// This class was mostly adapted from FARCenterQuery, part of FAR, by ferram4, GPLv3
+	// https://github.com/ferram4/Ferram-Aerospace-Research/blob/master/FerramAerospaceResearch/FARCenterQuery.cs
+    // Also see https://en.wikipedia.org/wiki/Resultant_force
+
+	// It accumulates forces and their points of applications, and provides methods for
+    // calculating the effective torque at any position, as well as the minimum-torque net force application point.
+    //
+    // The latter is a non-trivial issue; there is a 1-dimensional line of physically-equivalent solutions parallel
+    // to the resulting force vector; the solution closest to the weighted average of force positions is chosen.
+	// In the case of non-parallel forces, there usually is an infinite number of such lines, all of which have
+	// some amount of residual torque. The line with the least amount of residual torque is chosen.
+	public class ForceAccumulator
+	{
+		// Total force.
+		private Vector3d totalForce = Vector3d.zero;
+		// Torque needed to compensate if force were applied at origin.
+		private Vector3d totalZeroOriginTorque = Vector3d.zero;
+
+		// Weighted average of force application points.
+		private WeightedVectorAverager avgApplicationPoint = new WeightedVectorAverager();
+
+		// Feed an force to the accumulator.
+		public void AddForce(Vector3d applicationPoint, Vector3d force)
+		{
+			totalForce += force;
+			totalZeroOriginTorque += Vector3d.Cross(applicationPoint, force);
+			avgApplicationPoint.Add(applicationPoint, force.magnitude);
+		}
+
+        public Vector3d GetAverageForceApplicationPoint() {
+            return avgApplicationPoint.Get();
+        }
+
+        public void AddForce(AppliedForce force) {
+            AddForce(force.applicationPoint, force.vector);
+        }
+
+		// Residual torque for given force application point.
+		public Vector3d TorqueAt(Vector3d origin)
+		{
+			return totalZeroOriginTorque - Vector3d.Cross(origin, totalForce);
+		}
+
+        // Total force vector.
+        public Vector3d GetTotalForce()
+        {
+            return totalForce;
+        }
+
+        // Returns the minimum-residual-torque force application point that is closest to origin.
+        // Note that TorqueAt(GetMinTorquePos()) is always parallel to totalForce.
+        public Vector3d GetMinTorqueForceApplicationPoint(Vector3d origin)
+        {
+            double fmag = totalForce.sqrMagnitude;
+            if (fmag <= 0) {
+                return origin;
+            }
+
+            return origin + Vector3d.Cross(totalForce, TorqueAt(origin)) / fmag;
+        }
+
+        public Vector3d GetMinTorqueForceApplicationPoint()
+        {
+            return GetMinTorqueForceApplicationPoint(avgApplicationPoint.Get());
+        }
+	}
+}

--- a/KerbalEngineer/Helpers/Units.cs
+++ b/KerbalEngineer/Helpers/Units.cs
@@ -83,6 +83,11 @@
             return value.ToString("N" + decimals) + "Mm";
         }
 
+        public static string ToTorque(double value)
+        {
+            return value.ToString((value < 100.0) ? (Math.Abs(value) < Double.Epsilon) ? "N0" : "N1" : "N0") + "kNm";
+        }
+
         public static string ToForce(double value)
         {
             return value.ToString((value < 100000.0) ? (value < 10000.0) ? (value < 100.0) ? (Math.Abs(value) < Double.Epsilon) ? "N0" : "N3" : "N2" : "N1" : "N0") + "kN";

--- a/KerbalEngineer/VesselSimulator/EngineSim.cs
+++ b/KerbalEngineer/VesselSimulator/EngineSim.cs
@@ -38,6 +38,7 @@
         public bool isActive = false;
         public double isp = 0;
         public PartSim partSim;
+        public List<AppliedForce> appliedForces;
 
         public double thrust = 0;
 
@@ -58,7 +59,8 @@
                          bool throttleLocked,
                          List<Propellant> propellants,
                          bool active,
-                         bool correctThrust)
+                         bool correctThrust,
+                         List<Transform> thrustTransforms)
         {
             StringBuilder buffer = null;
             //MonoBehaviour.print("Create EngineSim for " + theEngine.name);
@@ -116,14 +118,14 @@
                 if (throttleLocked)
                 {
                     //MonoBehaviour.print("throttleLocked is true");
-                    flowRate = this.thrust / (this.isp * 9.81d);
+                    flowRate = this.thrust / (this.isp * 9.82);
                 }
                 else
                 {
                     if (this.partSim.isLanded)
                     {
                         //MonoBehaviour.print("partSim.isLanded is true, mainThrottle = " + FlightInputHandler.state.mainThrottle);
-                        flowRate = Math.Max(0.000001d, this.thrust * FlightInputHandler.state.mainThrottle) / (this.isp * 9.81d);
+                        flowRate = Math.Max(0.000001d, this.thrust * FlightInputHandler.state.mainThrottle) / (this.isp * 9.82);
                     }
                     else
                     {
@@ -136,12 +138,12 @@
                             }
 
                             //MonoBehaviour.print("requestedThrust > 0");
-                            flowRate = requestedThrust / (this.isp * 9.81d);
+                            flowRate = requestedThrust / (this.isp * 9.82);
                         }
                         else
                         {
                             //MonoBehaviour.print("requestedThrust <= 0");
-                            flowRate = this.thrust / (this.isp * 9.81d);
+                            flowRate = this.thrust / (this.isp * 9.82);
                         }
                     }
                 }
@@ -174,7 +176,7 @@
                     //MonoBehaviour.print("thrust at velocity = " + thrust);
                 }
 
-                flowRate = this.thrust / (this.isp * 9.81d);
+                flowRate = this.thrust / (this.isp * 9.82);
             }
 
             if (SimManager.logOutput)
@@ -212,6 +214,14 @@
             if (SimManager.logOutput)
             {
                 MonoBehaviour.print(buffer);
+            }
+
+            appliedForces = new List<AppliedForce>();
+            double thrustPerThrustTransform = thrust / thrustTransforms.Count;
+            foreach (Transform thrustTransform in thrustTransforms) {
+                Vector3d direction = thrustTransform.forward.normalized;
+                Vector3d position = thrustTransform.position;
+                appliedForces.Add(new AppliedForce(direction * thrustPerThrustTransform, position));
             }
         }
 

--- a/KerbalEngineer/VesselSimulator/PartSim.cs
+++ b/KerbalEngineer/VesselSimulator/PartSim.cs
@@ -35,6 +35,7 @@
     public class PartSim
     {
         private readonly List<AttachNodeSim> attachNodes = new List<AttachNodeSim>();
+        public Vector3d centerOfMass;
         public double baseMass = 0d;
         public double cost;
         public int decoupledInStage;
@@ -71,6 +72,7 @@
         public PartSim(Part thePart, int id, double atmosphere, LogMsg log)
         {
             this.part = thePart;
+            this.centerOfMass = thePart.transform.TransformPoint(thePart.CoMOffset);
             this.partId = id;
             this.name = this.part.partInfo.name;
 
@@ -206,7 +208,8 @@
                                                             engine.throttleLocked,
                                                             engine.propellants,
                                                             engine.isOperational,
-                                                            correctThrust);
+                                                            correctThrust,
+                                                            engine.thrustTransforms);
                         allEngines.Add(engineSim);
                     }
                 }
@@ -238,7 +241,8 @@
                                                             engine.throttleLocked,
                                                             engine.propellants,
                                                             engine.isOperational,
-                                                            correctThrust);
+                                                            correctThrust,
+                                                            engine.thrustTransforms);
                         allEngines.Add(engineSim);
                     }
                 }
@@ -268,7 +272,8 @@
                                                             engine.throttleLocked,
                                                             engine.propellants,
                                                             engine.isOperational,
-                                                            correctThrust);
+                                                            correctThrust,
+                                                            engine.thrustTransforms);
                         allEngines.Add(engineSim);
                     }
                 }

--- a/KerbalEngineer/VesselSimulator/Simulation.cs
+++ b/KerbalEngineer/VesselSimulator/Simulation.cs
@@ -32,8 +32,8 @@
 {
     public class Simulation
     {
-        private const double STD_GRAVITY = 9.81d;
-        private const double SECONDS_PER_DAY = 86400d;
+        private const double STD_GRAVITY = 9.82;
+        private const double SECONDS_PER_DAY = 86400;
         private readonly Stopwatch _timer = new Stopwatch();
         private List<EngineSim> activeEngines;
         private List<EngineSim> allEngines;
@@ -52,6 +52,7 @@
         private List<Part> partList;
         private double simpleTotalThrust;
         private double stageStartMass;
+        private Vector3d stageStartCom;
         private double stageTime;
         private double stepEndMass;
         private double stepStartMass;
@@ -59,6 +60,7 @@
         private double totalStageFlowRate;
         private double totalStageIspFlowRate;
         private double totalStageThrust;
+        private ForceAccumulator totalStageThrustForce;
         private Vector3 vecActualThrust;
         private Vector3 vecStageDeltaV;
         private Vector3 vecThrust;
@@ -101,6 +103,21 @@
                 }
 
                 return mass;
+            }
+        }
+
+        private Vector3d ShipCom
+        {
+            get
+            {
+                WeightedVectorAverager averager = new WeightedVectorAverager();
+
+                foreach (PartSim partSim in this.allParts)
+                {
+                    averager.Add(partSim.centerOfMass, partSim.GetMass());
+                }
+
+                return averager.Get();
             }
         }
 
@@ -262,6 +279,7 @@
             {
                 MonoBehaviour.print("RunSimulation started");
             }
+
             this._timer.Start();
 
             LogMsg log = null;
@@ -365,7 +383,10 @@
 
                 this.stageTime = 0d;
                 this.vecStageDeltaV = Vector3.zero;
+
                 this.stageStartMass = this.ShipMass;
+                this.stageStartCom = this.ShipCom;
+
                 this.stepStartMass = this.stageStartMass;
                 this.stepEndMass = 0;
 
@@ -381,6 +402,27 @@
                 stage.actualThrust = this.totalStageActualThrust;
                 stage.actualThrustToWeight = this.totalStageActualThrust / (this.stageStartMass * this.gravity);
 
+                // calculate torque and associates
+                stage.maxThrustTorque = this.totalStageThrustForce.TorqueAt(this.stageStartCom).magnitude;
+
+                // torque divided by thrust. imagine that all engines are at the end of a lever that tries to turn the ship.
+                // this numerical value, in meters, would represent the length of that lever.
+                double torqueLeverArmLength = (stage.thrust <= 0) ? 0 : stage.maxThrustTorque / stage.thrust;
+
+                // how far away are the engines from the CoM, actually?
+                double thrustDistance = (this.stageStartCom - this.totalStageThrustForce.GetAverageForceApplicationPoint()).magnitude;
+
+                // the combination of the above two values gives an approximation of the offset angle.
+                double sinThrustOffsetAngle = 0;
+                if (thrustDistance > 1e-7) {
+                    sinThrustOffsetAngle = torqueLeverArmLength / thrustDistance;
+                    if (sinThrustOffsetAngle > 1) {
+                        sinThrustOffsetAngle = 1;
+                    }
+                }
+
+                stage.thrustOffsetAngle = Math.Asin(sinThrustOffsetAngle) * 180 / Math.PI;
+
                 // Calculate the cost and mass of this stage and add all engines and tanks that are decoupled
                 // in the next stage to the dontStageParts list
                 foreach (PartSim partSim in this.allParts)
@@ -512,7 +554,7 @@
                 // Zero stage time if more than a day (this should be moved into the window code)
                 stage.time = (this.stageTime < SECONDS_PER_DAY) ? this.stageTime : 0d;
                 stage.number = this.doingCurrent ? -1 : this.currentStage; // Set the stage number to -1 if doing current engines
-                stage.partCount = this.allParts.Count;
+                stage.totalPartCount = this.allParts.Count;
                 stages[this.currentStage] = stage;
 
                 // Now activate the next stage
@@ -550,6 +592,7 @@
                     stages[i].totalMass += stages[j].mass;
                     stages[i].totalDeltaV += stages[j].deltaV;
                     stages[i].totalTime += stages[j].time;
+                    stages[i].partCount = i > 0 ? stages[i].totalPartCount - stages[i - 1].totalPartCount : stages[i].totalPartCount;
                 }
                 // We also total up the deltaV for stage and all stages below
                 for (int j = i; j < stages.Length; j++)
@@ -643,6 +686,7 @@
             this.totalStageActualThrust = 0d;
             this.totalStageFlowRate = 0d;
             this.totalStageIspFlowRate = 0d;
+            this.totalStageThrustForce = new ForceAccumulator();
 
             // Loop through all the active engines totalling the thrust, actual thrust and mass flow rates
             // The thrust is totalled as vectors
@@ -654,6 +698,10 @@
 
                 this.totalStageFlowRate += engine.ResourceConsumptions.Mass;
                 this.totalStageIspFlowRate += engine.ResourceConsumptions.Mass * engine.isp;
+
+                foreach (AppliedForce f in engine.appliedForces) {
+                    this.totalStageThrustForce.AddForce(f);
+                }
             }
 
             //MonoBehaviour.print("vecThrust = " + vecThrust.ToString() + "   magnitude = " + vecThrust.magnitude);

--- a/KerbalEngineer/VesselSimulator/Stage.cs
+++ b/KerbalEngineer/VesselSimulator/Stage.cs
@@ -45,8 +45,11 @@
         public double totalDeltaV = 0f;
         public double totalMass = 0f;
         public double totalTime = 0f;
+        public int totalPartCount = 0;
         public int partCount = 0;
         public double resourceMass = 0.0;
+        public double maxThrustTorque = 0f;
+        public double thrustOffsetAngle = 0f;
 
         public void Dump()
         {
@@ -64,6 +67,8 @@
             str.AppendFormat("thrustToWeight: {0:g6}\n", this.thrustToWeight);
             str.AppendFormat("maxTWR        : {0:g6}\n", this.maxThrustToWeight);
             str.AppendFormat("actualTWR     : {0:g6}\n", this.actualThrustToWeight);
+            str.AppendFormat("ThrustTorque  : {0:g6}\n", this.maxThrustTorque);
+            str.AppendFormat("ThrustOffset  : {0:g6}\n", this.thrustOffsetAngle);
             str.AppendFormat("deltaV        : {0:g6}\n", this.deltaV);
             str.AppendFormat("totalDeltaV   : {0:g6}\n", this.totalDeltaV);
             str.AppendFormat("invTotDeltaV  : {0:g6}\n", this.inverseTotalDeltaV);

 Binary files a/Output/KerbalEngineer/KerbalEngineer.dll and b/Output/KerbalEngineer/KerbalEngineer.dll differ
--- a/Output/KerbalEngineer/KerbalEngineer.version
+++ b/Output/KerbalEngineer/KerbalEngineer.version
@@ -6,7 +6,7 @@
 		"MAJOR":1,
 		"MINOR":0,
 		"PATCH":11,
-		"BUILD":0
+		"BUILD":3
 	},
 	"KSP_VERSION":
 	{