diff --git a/Src/PrecisionEngineering/Data/Calculations/Guides.cs b/Src/PrecisionEngineering/Data/Calculations/Guides.cs index cf4c171..367e606 100644 --- a/Src/PrecisionEngineering/Data/Calculations/Guides.cs +++ b/Src/PrecisionEngineering/Data/Calculations/Guides.cs @@ -195,7 +195,7 @@ public static void CalculateGuideLineDistance(NetToolProxy netTool, ICollection< var dist = Vector3.Distance(guideLine.Origin.Flatten(), guideLine.Intersect.Flatten()); var pos = Vector3Extensions.Average(guideLine.Origin, guideLine.Intersect); - measurements.Add(new DistanceMeasurement(dist, pos, true, guideLine.Origin, guideLine.Intersect, + measurements.Add(new DistanceMeasurement(dist, pos, true, guideLine.Origin, guideLine.Intersect, null, MeasurementFlags.HideOverlay | MeasurementFlags.Guide)); } } diff --git a/Src/PrecisionEngineering/Data/DistanceMeasurement.cs b/Src/PrecisionEngineering/Data/DistanceMeasurement.cs index 7add2b5..8e39c7a 100644 --- a/Src/PrecisionEngineering/Data/DistanceMeasurement.cs +++ b/Src/PrecisionEngineering/Data/DistanceMeasurement.cs @@ -5,13 +5,14 @@ namespace PrecisionEngineering.Data internal class DistanceMeasurement : Measurement { public DistanceMeasurement(float length, Vector3 position, bool isStraight, Vector3 startPosition, - Vector3 endPosition, MeasurementFlags flags) + Vector3 endPosition, float? curvatureRadius, MeasurementFlags flags) : base(position, flags) { Length = length; IsStraight = isStraight; StartPosition = startPosition; EndPosition = endPosition; + CurvatureRadius = curvatureRadius; } /// @@ -34,6 +35,11 @@ public DistanceMeasurement(float length, Vector3 position, bool isStraight, Vect /// public Vector3 EndPosition { get; } + /// + /// Radius of curvature at the point + /// + public float? CurvatureRadius { get; } + /// /// Difference in height between the StartPosition and EndPosition. /// diff --git a/Src/PrecisionEngineering/Data/Measurement.cs b/Src/PrecisionEngineering/Data/Measurement.cs index d9d79fa..0a92765 100644 --- a/Src/PrecisionEngineering/Data/Measurement.cs +++ b/Src/PrecisionEngineering/Data/Measurement.cs @@ -35,7 +35,12 @@ internal enum MeasurementFlags /// Indicate that this measurement is used for snapping and should be visable when /// snapping is enabled. /// - Snap = 1 << 6 + Snap = 1 << 6, + + /// + /// Indicate that this measurement should include a grade readout. + /// + Grade = 1 << 7, } internal abstract class Measurement diff --git a/Src/PrecisionEngineering/Data/PrecisionCalculator.cs b/Src/PrecisionEngineering/Data/PrecisionCalculator.cs index c400631..9a25ef0 100644 --- a/Src/PrecisionEngineering/Data/PrecisionCalculator.cs +++ b/Src/PrecisionEngineering/Data/PrecisionCalculator.cs @@ -3,6 +3,7 @@ using PrecisionEngineering.Detour; using PrecisionEngineering.Utilities; using UnityEngine; +using ColossalFramework.Math; namespace PrecisionEngineering.Data { @@ -45,8 +46,38 @@ public void Update(NetToolProxy netTool) CalculateControlPointDistances(netTool, _measurements); CalculateControlPointAngle(netTool, _measurements); - CalculateControlPointElevation(netTool, _measurements); + CalculateControlPointElevationAndCurveRadius(netTool, _measurements); CalculateCompassAngle(netTool, _measurements); + + CalculateArcLength(netTool, _measurements); + } + + /// + /// Calculates the arc length of a curved road, if there are three control points. + /// + private void CalculateArcLength(NetToolProxy netTool, List measurements) + { + if (netTool.ControlPointsCount != 2) + { + return; + } + + var p1 = netTool.ControlPoints[0]; + var p2 = netTool.ControlPoints[1]; + var p3 = netTool.ControlPoints[2]; + + var bezier = BezierUtil.CreateNetworkCurve(p1.m_position, p2.m_position, p3.m_position); + var center = bezier.Position(0.5f); + + //Approximate the length by measuring between a bunch of points. + float length = 0; + const int count = 16; + for(int i = 0; i < count; i++) + { + length += (bezier.Position(i / (float)count).Flatten() - bezier.Position((i + 1) / (float)count).Flatten()).magnitude; + } + + measurements.Add(new DistanceMeasurement(length, center, false, p1.m_position, p3.m_position, BezierUtil.FindCurvatureRadius(bezier, 0.5f), MeasurementFlags.Grade)); } /// @@ -67,7 +98,12 @@ private static void CalculateControlPointDistances(NetToolProxy netTool, ICollec var dist = Vector3.Distance(p1.Flatten(), p2.Flatten()); var pos = Vector3Extensions.Average(p1, p2); - measurements.Add(new DistanceMeasurement(dist, pos, true, p1, p2, MeasurementFlags.HideOverlay)); + var flags = MeasurementFlags.HideOverlay; + + if (netTool.ControlPointsCount == 1) //it's straight. + flags |= MeasurementFlags.Grade; + + measurements.Add(new DistanceMeasurement(dist, pos, true, p1, p2, null, flags)); } } @@ -184,7 +220,7 @@ private static void CalculateNearbySegments(NetToolProxy netTool, ICollection Settings.MinimumDistanceMeasure) { measurements.Add(new DistanceMeasurement(Vector3.Distance(p1, p), Vector3Extensions.Average(p1, p), true, - p1, p, + p1, p, null, MeasurementFlags.Secondary)); } } @@ -192,8 +228,20 @@ private static void CalculateNearbySegments(NetToolProxy netTool, ICollection /// Calculates the elevation of each control point. The last control point will be marked as primary, others as secondary. /// - private void CalculateControlPointElevation(NetToolProxy netTool, IList measurements) + private void CalculateControlPointElevationAndCurveRadius(NetToolProxy netTool, IList measurements) { + Bezier3 bezier = new Bezier3(); + bool isCurved = false; + if(netTool.ControlPointsCount == 2) + { + isCurved = true; + bezier = BezierUtil.CreateNetworkCurve( + netTool.ControlPoints[0].m_position, + netTool.ControlPoints[1].m_position, + netTool.ControlPoints[2].m_position + ); + } + for (var i = 0; i <= netTool.ControlPointsCount; i++) { // Only display the last control point elevation as a primary measurement @@ -206,8 +254,22 @@ private void CalculateControlPointElevation(NetToolProxy netTool, IList Settings.MinimumDistanceMeasure) { - dist = string.Format("H: {0}", StringUtil.GetHeightMeasurementString(dm.Length)); - } - else - { - dist = StringUtil.GetDistanceMeasurementString(dm.Length, _secondaryDetailEnabled); + if ((dm.Flags & MeasurementFlags.Height) != 0) + { + dist = string.Format("H: {0}", StringUtil.GetHeightMeasurementString(dm.Length)); - if (_secondaryDetailEnabled) + } + else { - var heightdiff = (int) dm.RelativeHeight.RoundToNearest(1); + dist = StringUtil.GetDistanceMeasurementString(dm.Length, _secondaryDetailEnabled); - if (Mathf.Abs(heightdiff) > 0) + if (_secondaryDetailEnabled) { - dist += string.Format("\n(Elev: {0})", StringUtil.GetHeightMeasurementString(heightdiff)); + var heightdiff = (int)dm.RelativeHeight.RoundToNearest(1); + + if (Mathf.Abs(heightdiff) > 0) + { + dist += string.Format("\n(Elev: {0})", StringUtil.GetHeightMeasurementString(heightdiff)); + if ((dm.Flags & MeasurementFlags.Grade) != 0) + dist += string.Format("\n(Grade: {0})", (heightdiff / dm.Length).ToString("P02")); + } } } } + if (_secondaryDetailEnabled && dm.CurvatureRadius.HasValue) + { + //We only need to add a newline if there's already text in there. + if (dist.Length > 0) + dist += "\n"; + dist += string.Format("(Radius: {0})", StringUtil.GetDistanceMeasurementString(dm.CurvatureRadius.Value, _secondaryDetailEnabled)); + } + label.SetValue(dist); label.SetWorldPosition(cameraInfo, DistanceRenderer.GetLabelWorldPosition(dm)); diff --git a/Src/PrecisionEngineering/Utilities/BezierUtil.cs b/Src/PrecisionEngineering/Utilities/BezierUtil.cs index 6eec997..5057e93 100644 --- a/Src/PrecisionEngineering/Utilities/BezierUtil.cs +++ b/Src/PrecisionEngineering/Utilities/BezierUtil.cs @@ -107,11 +107,158 @@ private static Bezier3 CreateSmallArc(float r, float a1, float a2) return new Bezier3 { - a = new Vector3(r*Mathf.Cos(a1), 0, r*Mathf.Sin(a1)), - b = new Vector3(x2*cos_ar - y2*sin_ar, 0, x2*sin_ar + y2*cos_ar), - c = new Vector3(x3*cos_ar - y3*sin_ar, 0, x3*sin_ar + y3*cos_ar), - d = new Vector3(r*Mathf.Cos(a2), 0, r*Mathf.Sin(a2)) + a = new Vector3(r * Mathf.Cos(a1), 0, r * Mathf.Sin(a1)), + b = new Vector3(x2 * cos_ar - y2 * sin_ar, 0, x2 * sin_ar + y2 * cos_ar), + c = new Vector3(x3 * cos_ar - y3 * sin_ar, 0, x3 * sin_ar + y3 * cos_ar), + d = new Vector3(r * Mathf.Cos(a2), 0, r * Mathf.Sin(a2)) }; } + + public static Bezier3 CreateNetworkCurve(Vector3 p1, Vector3 p2, Vector3 p3) + { + //Beziers take 4 points, not 3, so we make a crude approximation of the two midle controll points here + var c1 = (p2 - p1) * 0.552f + p1; + var c2 = (p2 - p3) * 0.552f + p3; + + return new Bezier3(p1, c1, c2, p3); + } + + public static float FindCurvatureRadius(Bezier3 bezier, float t) + { + const float delta = 0.01f; + float t1, t2, t3; + if(t < delta*2) + { + t1 = t; + t2 = t + delta; + t3 = t + delta * 2; + } + else if (t > 1 - delta * 2) + { + t1 = t; + t2 = t - delta; + t3 = t - delta * 2; + } + else + { + t1 = t - delta; + t2 = t; + t3 = t + delta; + } + var p1 = bezier.Position(t1).Flatten(); + var p2 = bezier.Position(t2).Flatten(); + var p3 = bezier.Position(t3).Flatten(); + + FindCircle(p1, p2, p3, out var center, out var radius); + + //If the curve radius is big enough, we may as well ignore it. + if (radius > 10000) + radius = float.NaN; + + return radius; + } + // Find a circle through the three points. + private static void FindCircle(Vector3 a, Vector3 b, Vector3 c, + out Vector3 center, out float radius) + { + // Get the perpendicular bisector of (x1, y1) and (x2, y2). + float x1 = (b.x + a.x) / 2; + float y1 = (b.z + a.z) / 2; + float dy1 = b.x - a.x; + float dx1 = -(b.z - a.z); + + // Get the perpendicular bisector of (x2, y2) and (x3, y3). + float x2 = (c.x + b.x) / 2; + float y2 = (c.z + b.z) / 2; + float dy2 = c.x - b.x; + float dx2 = -(c.z - b.z); + + // See where the lines intersect. + bool lines_intersect, segments_intersect; + Vector3 intersection, close1, close2; + FindIntersection( + new Vector3(x1, 0, y1), new Vector3(x1 + dx1, 0, y1 + dy1), + new Vector3(x2, 0, y2), new Vector3(x2 + dx2, 0, y2 + dy2), + out lines_intersect, out segments_intersect, + out intersection, out close1, out close2); + if (!lines_intersect) + { + center = new Vector3(0, 0, 0); + radius = float.NaN; + } + else + { + center = intersection; + float dx = center.x - a.x; + float dy = center.z - a.z; + radius = (float)Mathf.Sqrt(dx * dx + dy * dy); + } + } + // Find the point of intersection between + // the lines p1 --> p2 and p3 --> p4. + private static void FindIntersection( + Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4, + out bool lines_intersect, out bool segments_intersect, + out Vector3 intersection, + out Vector3 close_p1, out Vector3 close_p2) + { + // Get the segments' parameters. + float dx12 = p2.x - p1.x; + float dy12 = p2.z - p1.z; + float dx34 = p4.x - p3.x; + float dy34 = p4.z - p3.z; + + // Solve for t1 and t2 + float denominator = (dy12 * dx34 - dx12 * dy34); + + float t1 = + ((p1.x - p3.x) * dy34 + (p3.z - p1.z) * dx34) + / denominator; + if (float.IsInfinity(t1)) + { + // The lines are parallel (or close enough to it). + lines_intersect = false; + segments_intersect = false; + intersection = new Vector3(float.NaN, float.NaN, float.NaN); + close_p1 = new Vector3(float.NaN, float.NaN, float.NaN); + close_p2 = new Vector3(float.NaN, float.NaN, float.NaN); + return; + } + lines_intersect = true; + + float t2 = + ((p3.x - p1.x) * dy12 + (p1.z - p3.z) * dx12) + / -denominator; + + // Find the point of intersection. + intersection = new Vector3(p1.x + dx12 * t1, 0, p1.z + dy12 * t1); + + // The segments intersect if t1 and t2 are between 0 and 1. + segments_intersect = + ((t1 >= 0) && (t1 <= 1) && + (t2 >= 0) && (t2 <= 1)); + + // Find the closest points on the segments. + if (t1 < 0) + { + t1 = 0; + } + else if (t1 > 1) + { + t1 = 1; + } + + if (t2 < 0) + { + t2 = 0; + } + else if (t2 > 1) + { + t2 = 1; + } + + close_p1 = new Vector3(p1.x + dx12 * t1, 0, p1.z + dy12 * t1); + close_p2 = new Vector3(p3.x + dx34 * t2, 0, p3.z + dy34 * t2); + } } }