Merge pull request #9 from mic-e/thrustdirectionerror
Merge pull request #9 from mic-e/thrustdirectionerror

thrust torque + angle readout

--- 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/BuildOverlayPartInfo.cs
+++ b/KerbalEngineer/Editor/BuildOverlayPartInfo.cs
@@ -317,9 +317,9 @@
 
             var reactionWheel = this.selectedPart.GetModule<ModuleReactionWheel>();
             this.infoItems.Add(new PartInfoItem("Reaction Wheel Torque"));
-            this.infoItems.Add(new PartInfoItem("\tPitch", reactionWheel.PitchTorque.ToForce()));
-            this.infoItems.Add(new PartInfoItem("\tRoll", reactionWheel.RollTorque.ToForce()));
-            this.infoItems.Add(new PartInfoItem("\tYaw", reactionWheel.YawTorque.ToForce()));
+            this.infoItems.Add(new PartInfoItem("\tPitch", reactionWheel.PitchTorque.ToTorque()));
+            this.infoItems.Add(new PartInfoItem("\tRoll", reactionWheel.RollTorque.ToTorque()));
+            this.infoItems.Add(new PartInfoItem("\tYaw", reactionWheel.YawTorque.ToTorque()));
             foreach (var resource in reactionWheel.inputResources)
             {
                 this.infoItems.Add(new PartInfoItem("\t" + resource.name, resource.rate.ToRate()));

--- 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/Extensions/FloatExtensions.cs
+++ b/KerbalEngineer/Extensions/FloatExtensions.cs
@@ -49,6 +49,11 @@
             return Units.ToForce(value);
         }
 
+        public static string ToTorque(this float value)
+        {
+            return Units.ToTorque(value);
+        }
+
         public static string ToMass(this float value)
         {
             return Units.ToMass(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;
@@ -74,7 +76,7 @@
             }
         }
 
-        private double ShipStartMass
+        private double ShipMass
         {
             get
             {
@@ -82,25 +84,25 @@
 
                 foreach (PartSim partSim in this.allParts)
                 {
-                    mass += partSim.GetStartMass();
+                    mass += partSim.GetMass();
                 }
 
                 return mass;
             }
         }
 
-        private double ShipMass
+        private Vector3d ShipCom
         {
             get
             {
-                double mass = 0d;
+                WeightedVectorAverager averager = new WeightedVectorAverager();
 
                 foreach (PartSim partSim in this.allParts)
                 {
-                    mass += partSim.GetMass();
-                }
-
-                return mass;
+                    averager.Add(partSim.centerOfMass, partSim.GetMass());
+                }
+
+                return averager.Get();
             }
         }
 
@@ -262,6 +264,7 @@
             {
                 MonoBehaviour.print("RunSimulation started");
             }
+
             this._timer.Start();
 
             LogMsg log = null;
@@ -365,7 +368,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 +387,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 +539,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 +577,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 +671,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 +683,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":
 	{