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);
+ }
}
}