diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f1092a94..e2164e091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,7 @@ - `Ellipse` - `EllipticalArc` - `IndexedPolycurve` +- `IHasArcLength` - `Grid1d.GetCellDomains` - `Message.Info` - `Message.Error` @@ -90,21 +91,33 @@ - `Topography.Trimmed` - `new Topography(Topography other)` - `Topography.TopMesh()` -- `UpdateElementRepresentations` flag to all serialization methods +- `Bezier.PointAtLength()` +- `Bezier.PointAtNormalizedLength()` +- `Bezier.ParameterAt()` +- `Bezier.DivideByLength()` +- `Bezier.Split()` +- `Bezier.SplitAt()` +- `Bezier.SplitByLength()` +- `Bezier.ConstructPiecewiseCubicBezier()` ### Changed - `Polyline` now inherits from `BoundedCurve`. - `Polyline` is now parameterized 0->length. +- `Polyline` now implements the `IHasArcLength` interface. - `Arc` now inherits from `TrimmedCurve`. -- `Arc` is now parameterized 0->2Pi -- `Arc` now automatically corrects decreasing angle domains to be increasing, while preserving direction. +- `Arc` is now parameterized 0->2Pi. - `Line` now inherits from `TrimmedCurve`. - `Line` is now parameterized 0->length. +- `Line` now implements the `IHasArcLength` interface. - `Bezier` now inherits from `BoundedCurve`. +- `Bezier` now implements the `IHasArcLength` interface. +- `Bezier.ArcLength()` now uses Gauss quadrature approximation vs linear sampling. +- `Bezier` now implements the `IHasArcLength` interface. +- `Bezier.ArcLength()` now uses Gauss quadrature approximation vs linear sampling. - `Polyline` is now parameterized 0->length. - `Circle` is now parameterized 0->2Pi. -- `Line` is now parameterized 0->length. +- `Circle` now implements the `IHasArcLength` interface. - `Vector3.DistanceTo(Ray ray)` now returns positive infinity instead of throwing. - `Message`: removed obsolete `FromLine` method. - `AdaptiveGrid`: removed obsolete `TryGetVertexIndex` with `tolerance` parameter. diff --git a/Elements.MEP/src/Fittings/Reducer.cs b/Elements.MEP/src/Fittings/Reducer.cs index 2b48a7195..a80ce92d4 100644 --- a/Elements.MEP/src/Fittings/Reducer.cs +++ b/Elements.MEP/src/Fittings/Reducer.cs @@ -36,7 +36,7 @@ public static Reducer ReducerForPipe(StraightSegment pipe, double reducerLength, var path = reducerAtEnd ? pipe.Path.Segments()[0].Reversed() : pipe.Path.Segments()[0]; - var position = path.DivideByLength(distanceFromEnd)[0].End; + var position = path.DivideByLengthToSegments(distanceFromEnd)[0].End; var orientation = path.Direction(); // var fittingMaterial = new Material("green", new Color(0, 1, 0, 0.5); @@ -133,7 +133,7 @@ public void Move(Vector3 translation) } /// - /// Port with smaller diameter points to the +X axis. + /// Port with smaller diameter points to the +X axis. /// If there is eccentric transform, the smaller part will be shifted to the -Z axis. /// We point smaller diameter in the +X direction so that there is one reducer defined in the standard orientation, to which this transformation is then applied. /// This let's us just have one size 110/90 that is rotated into a 90/110 orientation when needed. diff --git a/Elements/src/Elements.csproj b/Elements/src/Elements.csproj index cdd21865a..8bc4905ea 100644 --- a/Elements/src/Elements.csproj +++ b/Elements/src/Elements.csproj @@ -10,7 +10,7 @@ The Elements library provides object types for generating the built environment. https://github.com/hypar-io/elements https://github.com/hypar-io/elements - $(Version) + 21.21.21 en diff --git a/Elements/src/Geometry/Bezier.cs b/Elements/src/Geometry/Bezier.cs index c9fbad96a..d99e639de 100644 --- a/Elements/src/Geometry/Bezier.cs +++ b/Elements/src/Geometry/Bezier.cs @@ -11,7 +11,7 @@ namespace Elements.Geometry // http://webhome.cs.uvic.ca/~blob/courses/305/notes/pdf/ref-frames.pdf /// - /// The frame type to be used for operations requiring + /// The frame type to be used for operations requiring /// a moving frame around the curve. /// public enum FrameType @@ -119,11 +119,170 @@ public override double ArcLength(double start, double end) throw new ArgumentOutOfRangeException("end", $"The end parameter {end} must be between {Domain.Min} and {Domain.Max}."); } + // if we have a true Bezier, calculate a more accurate ArcLength using Gauss weights + if (ControlPoints.Count == 2) + { + return BezierArcLength(start, end); + } + // TODO: We use max value here so that the calculation will continue // until at least the end of the curve. This is not a nice solution. return ArcLengthUntil(start, double.MaxValue, out end); } + /// + /// Constants for Gauss quadrature points and weights (n = 24) + /// https://pomax.github.io/bezierinfo/legendre-gauss.html + /// + private static readonly double[] T = new double[] + { + -0.0640568928626056260850430826247450385909, + 0.0640568928626056260850430826247450385909, + -0.1911188674736163091586398207570696318404, + 0.1911188674736163091586398207570696318404, + -0.3150426796961633743867932913198102407864, + 0.3150426796961633743867932913198102407864, + -0.4337935076260451384870842319133497124524, + 0.4337935076260451384870842319133497124524, + -0.5454214713888395356583756172183723700107, + 0.5454214713888395356583756172183723700107, + -0.6480936519369755692524957869107476266696, + 0.6480936519369755692524957869107476266696, + -0.7401241915785543642438281030999784255232, + 0.7401241915785543642438281030999784255232, + -0.8200019859739029219539498726697452080761, + 0.8200019859739029219539498726697452080761, + -0.8864155270044010342131543419821967550873, + 0.8864155270044010342131543419821967550873, + -0.9382745520027327585236490017087214496548, + 0.9382745520027327585236490017087214496548, + -0.9747285559713094981983919930081690617411, + 0.9747285559713094981983919930081690617411, + -0.9951872199970213601799974097007368118745, + 0.9951872199970213601799974097007368118745 + }; + + /// + /// Constants for Gauss quadrature weights corresponding to the points (n = 24) + /// https://pomax.github.io/bezierinfo/legendre-gauss.html + /// + private static readonly double[] C = new double[] + { + 0.1279381953467521569740561652246953718517, + 0.1279381953467521569740561652246953718517, + 0.1258374563468282961213753825111836887264, + 0.1258374563468282961213753825111836887264, + 0.121670472927803391204463153476262425607, + 0.121670472927803391204463153476262425607, + 0.1155056680537256013533444839067835598622, + 0.1155056680537256013533444839067835598622, + 0.1074442701159656347825773424466062227946, + 0.1074442701159656347825773424466062227946, + 0.0976186521041138882698806644642471544279, + 0.0976186521041138882698806644642471544279, + 0.086190161531953275917185202983742667185, + 0.086190161531953275917185202983742667185, + 0.0733464814110803057340336152531165181193, + 0.0733464814110803057340336152531165181193, + 0.0592985849154367807463677585001085845412, + 0.0592985849154367807463677585001085845412, + 0.0442774388174198061686027482113382288593, + 0.0442774388174198061686027482113382288593, + 0.0285313886289336631813078159518782864491, + 0.0285313886289336631813078159518782864491, + 0.0123412297999871995468056670700372915759, + 0.0123412297999871995468056670700372915759 + }; + + /// + /// Computes the arc length of the Bézier curve between the given parameter values start and end. + /// https://pomax.github.io/bezierinfo/#arclength + /// + /// The starting parameter value of the Bézier curve. + /// The ending parameter value of the Bézier curve. + /// The arc length between the specified parameter values. + public double BezierArcLength(double start, double end) + { + double z = 0.5; // Scaling factor for the Legendre-Gauss quadrature + int len = T.Length; // Number of points in the Legendre-Gauss quadrature + + double sum = 0; // Accumulated sum for the arc length calculation + + // Iterating through the Legendre-Gauss quadrature points and weights + for (int i = 0; i < len; i++) + { + double t = z * T[i] + z; // Mapping the quadrature point to the Bézier parameter range [0, 1] + Vector3 derivative = Derivative(t); // Calculating the derivative of the Bézier curve at parameter t + sum += C[i] * ArcFn(t, derivative); // Adding the weighted arc length contribution to the sum + } + + // Scaling the sum by the scaling factor and the parameter interval (end - start) to get the arc length between start and end. + return z * sum * (end - start); + } + + /// + /// Calculates the arc length contribution at parameter t based on the derivative of the Bézier curve. + /// + /// The parameter value of the Bézier curve. + /// The derivative of the Bézier curve at parameter t as a Vector3. + /// The arc length contribution at parameter t. + private double ArcFn(double t, Vector3 d) + { + // Compute the Euclidean distance of the derivative vector (d) at parameter t + return Math.Sqrt(d.X * d.X + d.Y * d.Y); + } + + /// + /// Computes the derivative of the Bézier curve at parameter t. + /// https://pomax.github.io/bezierinfo/#derivatives + /// + /// The parameter value of the Bézier curve. + /// The derivative of the Bézier curve as a Vector3. + private Vector3 Derivative(double t) + { + int n = ControlPoints.Count - 1; // Degree of the Bézier curve + Vector3[] derivatives = new Vector3[n]; // Array to store the derivative control points + + // Calculating the derivative control points using the given formula + for (int i = 0; i < n; i++) + { + derivatives[i] = n * (ControlPoints[i + 1] - ControlPoints[i]); + } + + // Using the derivative control points to construct an (n-1)th degree Bézier curve at parameter t. + return BezierCurveValue(t, derivatives); + } + + /// + /// Evaluates the value of an (n-1)th degree Bézier curve at parameter t using the given control points. + /// + /// The parameter value of the Bézier curve. + /// The control points for the Bézier curve. + /// The value of the Bézier curve at parameter t as a Vector3. + private Vector3 BezierCurveValue(double t, Vector3[] controlPoints) + { + int n = controlPoints.Length - 1; // Degree of the Bézier curve + Vector3[] points = new Vector3[n + 1]; + + // Initialize the points array with the provided control points + for (int i = 0; i <= n; i++) + { + points[i] = controlPoints[i]; + } + + // De Casteljau's algorithm to evaluate the value of the Bézier curve at parameter t + for (int r = 1; r <= n; r++) + { + for (int i = 0; i <= n - r; i++) + { + points[i] = (1 - t) * points[i] + t * points[i + 1]; + } + } + + // The first element of the points array contains the value of the Bézier curve at parameter t. + return points[0]; + } + private double ArcLengthUntil(double start, double distance, out double end) { Vector3 last = new Vector3(); @@ -151,6 +310,15 @@ private double ArcLengthUntil(double start, double distance, out double end) return length; } + /// + /// The mid point of the curve. + /// + /// The length based midpoint. + public virtual Vector3 MidPoint() + { + return PointAtNormalizedLength(0.5); + } + /// /// Get the point on the curve at parameter u. /// @@ -170,6 +338,157 @@ public override Vector3 PointAt(double u) return p; } + /// + /// Returns the point on the bezier corresponding to the specified length value. + /// + /// The length value along the bezier. + /// The point on the bezier corresponding to the specified length value. + /// Thrown when the specified length is out of range. + public virtual Vector3 PointAtLength(double length) + { + double totalLength = ArcLength(this.Domain.Min, this.Domain.Max); // Calculate the total length of the Bezier + if (length < 0 || length > totalLength) + { + throw new ArgumentException("The specified length is out of range."); + } + return PointAt(ParameterAtDistanceFromParameter(length, Domain.Min)); + } + + /// + /// Returns the point on the bezier corresponding to the specified normalized length-based parameter value. + /// + /// The normalized length-based parameter value, ranging from 0 to 1. + /// The point on the bezier corresponding to the specified normalized length-based parameter value. + /// Thrown when the specified parameter is out of range. + public virtual Vector3 PointAtNormalizedLength(double parameter) + { + if (parameter < 0 || parameter > 1) + { + throw new ArgumentException("The specified parameter is out of range."); + } + return PointAtLength(parameter * this.ArcLength(this.Domain.Min, this.Domain.Max)); + } + + /// + /// Finds the parameter value on the Bezier curve that corresponds to the given 3D point within a specified threshold. + /// + /// The 3D point to find the corresponding parameter for. + /// The maximum distance threshold to consider a match between the projected point and the original point. + /// The parameter value on the Bezier curve if the distance between the projected point and the original point is within the threshold, otherwise returns null. + public double? ParameterAt(Vector3 point, double threshold = 0.0001) + { + // Find the parameter corresponding to the projected point on the Bezier curve + var parameter = ProjectedPoint(point, threshold); + + if (parameter == null) + { + // If the projected point does not return a relevant parameter return null + return null; + } + // Find the 3D point on the Bezier curve at the obtained parameter value + var projection = PointAt((double)parameter); + + // Check if the distance between the projected point and the original point is within + // a tolerence of the threshold + if (projection.DistanceTo(point) < (threshold * 10) - threshold) + { + // If the distance is within the threshold, return the parameter value + return parameter; + } + else + { + // If the distance exceeds the threshold, consider the point as not on the Bezier curve and return null + return null; + } + } + + /// + /// Projects a 3D point onto the Bezier curve to find the parameter value of the closest point on the curve. + /// + /// The 3D point to project onto the Bezier curve. + /// The maximum threshold to refine the projection and find the closest point. + /// The parameter value on the Bezier curve corresponding to the projected point, or null if the projection is not within the specified threshold. + public double? ProjectedPoint(Vector3 point, double threshold = 0.001) + { + // https://pomax.github.io/bezierinfo/#projections + // Generate a lookup table (LUT) of points and their corresponding parameter values on the Bezier curve + List<(Vector3 point, double t)> lut = GenerateLookupTable(); + + // Initialize variables to store the closest distance (d) and the index (index) of the closest point in the lookup table + double d = double.MaxValue; + int index = 0; + + // Find the closest point to the input point in the lookup table (LUT) using Euclidean distance + for (int i = 0; i < lut.Count; i++) + { + double q = Math.Sqrt(Math.Pow((point - lut[i].point).X, 2) + Math.Pow((point - lut[i].point).Y, 2) + Math.Pow((point - lut[i].point).Z, 2)); + if (q < d) + { + d = q; + index = i; + } + } + + // Obtain the parameter values of the neighboring points in the LUT for further refinement + double t1 = lut[Math.Max(index - 1, 0)].t; + double t2 = lut[Math.Min(index + 1, lut.Count - 1)].t; + double v = t2 - t1; + + // Refine the projection by iteratively narrowing down the parameter range to find the closest point + while (v > threshold) + { + // Calculate intermediate parameter values + double t0 = t1 + v / 4; + double t3 = t2 - v / 4; + + // Calculate corresponding points on the Bezier curve using the intermediate parameter values + Vector3 p0 = PointAt(t0); + Vector3 p3 = PointAt(t3); + + // Calculate the distances between the input point and the points on the Bezier curve + double d0 = Math.Sqrt(Math.Pow((point - p0).X, 2) + Math.Pow((point - p0).Y, 2) + Math.Pow((point - p0).Z, 2)); + double d3 = Math.Sqrt(Math.Pow((point - p3).X, 2) + Math.Pow((point - p3).Y, 2) + Math.Pow((point - p3).Z, 2)); + + // Choose the sub-range that is closer to the input point and update the range + if (d0 < d3) + { + t2 = t3; + } + else + { + t1 = t0; + } + + // Update the range difference for the next iteration + v = t2 - t1; + } + + // Return the average of the refined parameter values as the projection of the input point on the Bezier curve + return (t1 + t2) / 2; + } + + /// + /// Generates a lookup table of points and their corresponding parameter values on the Bezier curve. + /// + /// Number of samples to take along the curve. + /// A list of tuples containing the sampled points and their corresponding parameter values on the Bezier curve. + private List<(Vector3 point, double t)> GenerateLookupTable(int numSamples = 100) + { + // Initialize an empty list to store the lookup table (LUT) + List<(Vector3 point, double t)> lut = new List<(Vector3 point, double t)>(); + + // Generate lookup table by sampling points on the Bezier curve + for (int i = 0; i <= numSamples; i++) + { + double t = (double)i / numSamples; // Calculate the parameter value based on the current sample index + Vector3 point = PointAt(t); // Get the 3D point on the Bezier curve corresponding to the current parameter value + lut.Add((point, t)); // Add the sampled point and its corresponding parameter value to the lookup table (LUT) + } + + // Return the completed lookup table (LUT) + return lut; + } + private double BinomialCoefficient(int n, int i) { return Factorial(n) / (Factorial(i) * Factorial(n - i)); @@ -344,6 +663,278 @@ public override double ParameterAtDistanceFromParameter(double distance, double return end; } + /// + /// Divides the bezier into segments of the specified length. + /// + /// The desired length of each segment. + /// A list of points representing the segment divisions. + public Vector3[] DivideByLength(double divisionLength) + { + var totalLength = this.ArcLength(Domain.Min, Domain.Max); + if (totalLength <= 0) + { + // Handle invalid bezier with insufficient length + return new Vector3[0]; + } + var parameter = ParameterAtDistanceFromParameter(divisionLength, Domain.Min); + var segments = new List { this.Start }; + + while (parameter < Domain.Max) + { + segments.Add(PointAt(parameter)); + var newParameter = ParameterAtDistanceFromParameter(divisionLength, parameter); + parameter = newParameter != parameter ? newParameter : Domain.Max; + } + + // Add the last vertex of the bezier as the endpoint of the last segment if it + // is not already part of the list + if (!segments[segments.Count - 1].IsAlmostEqualTo(this.End)) + { + segments.Add(this.End); + } + + return segments.ToArray(); + } + + /// + /// Divides the bezier into segments of the specified length. + /// + /// The desired length of each segment. + /// A list of beziers representing the segments. + public List SplitByLength(double divisionLength) + { + var totalLength = this.ArcLength(Domain.Min, Domain.Max); + if (totalLength <= 0) + { + // Handle invalid bezier with insufficient length + return null; + } + var currentParameter = ParameterAtDistanceFromParameter(divisionLength, Domain.Min); + var parameters = new List { this.Domain.Min }; + + while (currentParameter < Domain.Max) + { + parameters.Add(currentParameter); + var newParameter = ParameterAtDistanceFromParameter(divisionLength, currentParameter); + currentParameter = newParameter != currentParameter ? newParameter : Domain.Max; + } + + // Add the last vertex of the bezier as the endpoint of the last segment if it + // is not already part of the list + if (!parameters[parameters.Count - 1].ApproximatelyEquals(this.Domain.Max)) + { + parameters.Add(this.Domain.Max); + } + + return Split(parameters); + } + + /// + /// Splits the Bezier curve into segments at specified parameter values. + /// + /// The list of parameter values to split the curve at. + /// If true the parameters will be length normalized. + /// A list of Bezier segments obtained after splitting. + public List Split(List parameters, bool normalize = false) + { + // Calculate the total length of the Bezier curve + var totalLength = this.ArcLength(Domain.Min, Domain.Max); + + // Check for invalid curve with insufficient length + if (totalLength <= 0) + { + throw new InvalidOperationException($"Invalid bezier with insufficient length. Total Length = {totalLength}"); + } + + // Check if the list of parameters is empty or null + if (parameters == null || parameters.Count == 0) + { + throw new ArgumentException("No split points provided."); + } + + // Initialize a list to store the resulting Bezier segments + var segments = new List(); + var bezier = this; // Create a reference to the original Bezier curve + + if (normalize) + { + parameters = parameters.Select(parameter => ParameterAtDistanceFromParameter(parameter * this.ArcLength(this.Domain.Min, this.Domain.Max), Domain.Min)).ToList(); + } + parameters.Sort(); // Sort the parameters in ascending order + + // Iterate through each parameter to split the curve + for (int i = 0; i < parameters.Count; i++) + { + double t = (Domain.Min <= parameters[i] && parameters[i] <= Domain.Max) + ? parameters[i] // Ensure the parameter is within the domain + : throw new ArgumentException($"Parameter {parameters[i]} is not within the domain ({Domain.Min}->{Domain.Max}) of the Bezier curve."); + + // Check if the parameter is within the valid range [0, 1] + if (t >= 0 && t <= 1) + { + // Split the curve at the given parameter and obtain the two resulting Bezier segments + var tuple = bezier.SplitAt(t); + + // Store the first split Bezier in the list + segments.Add(tuple.Item1); + + // Update bezier to the second split Bezier to continue splitting + bezier = tuple.Item2; + + // Remap subsequent parameters to the new Bezier curve's parameter space + for (int j = i + 1; j < parameters.Count; j++) + { + parameters[j] = (parameters[j] - t) / (1 - t); + } + } + } + + segments.Add(bezier); + + // Return the list of Bezier segments obtained after splitting + return segments; + } + + /// + /// Splits the bezier curve at the given parameter value. + /// + /// The parameter value at which to split the curve. + /// A tuple containing two split bezier curves. + public Tuple SplitAt(double t) + { + // Extract the control points from the input bezier + var startPoint = ControlPoints[0]; + var controlPoint1 = ControlPoints[1]; + var controlPoint2 = ControlPoints[2]; + var endPoint = ControlPoints[3]; + + // Compute the intermediate points using de Casteljau's algorithm + var q0 = (1 - t) * startPoint + t * controlPoint1; + var q1 = (1 - t) * controlPoint1 + t * controlPoint2; + var q2 = (1 - t) * controlPoint2 + t * endPoint; + + var r0 = (1 - t) * q0 + t * q1; + var r1 = (1 - t) * q1 + t * q2; + + // Compute the split point on the bezier curve + var splitPoint = (1 - t) * r0 + t * r1; + + // Construct the first split bezier curve + var subBezier1 = new Bezier(new List() { startPoint, q0, r0, splitPoint }); + + // Construct the second split bezier curve + var subBezier2 = new Bezier(new List() { splitPoint, r1, q2, endPoint }); + + // Return a tuple containing the split bezier curves + return new Tuple(subBezier1, subBezier2); + } + + /// + /// Constructs piecewise cubic Bézier curves from a list of points using control points calculated with the specified looseness. + /// + /// The list of points defining the path. + /// The looseness factor used to calculate control points. A higher value results in smoother curves. + /// If true, the path will be closed, connecting the last point with the first one. + /// A list of piecewise cubic Bézier curves approximating the path defined by the input points. + public static List ConstructPiecewiseCubicBezier(List points, double looseness = 6.0, bool close = false) + { + List beziers = new List(); + + // Calculate the control points. + List controlPoints = CalculateControlPoints(points, looseness); + + // Create the start Bezier curve. + Bezier startBezier = new Bezier( + new List + { + points[0], + controlPoints[0][1], + points[1] + } + ); + + // Add the start Bezier curve to the list. + beziers.Add(startBezier); + + // Iterate through pairs of points. + for (int i = 1; i < points.Count - 2; i++) + { + // Create the control points. + List bezierControlPoints = new List + { + points[i], + controlPoints[i - 1][0], + controlPoints[i][1], + points[i + 1] + }; + + // Create the Bezier curve. + Bezier bezier = new Bezier(bezierControlPoints); + + // Add the Bezier curve to the list. + beziers.Add(bezier); + } + + // Create the end Bezier curve. + Bezier endBezier = new Bezier( + new List + { + points[points.Count() - 1], + controlPoints[controlPoints.Count() - 1][0], + points[points.Count() - 2] + } + ); + + // Add the end Bezier curve to the list. + beziers.Add(endBezier); + + // Return the list of Bezier curves. + return beziers; + } + + /// + /// Calculates the control points for constructing piecewise cubic Bézier curves from the given list of points and looseness factor. + /// + /// The list of points defining the path. + /// The looseness factor used to calculate control points. A higher value results in smoother curves. + /// A list of control points (pairs of Vector3) for the piecewise cubic Bézier curves. + private static List CalculateControlPoints(List points, double looseness) + { + List controlPoints = new List(); + + for (int i = 1; i < points.Count - 1; i++) + { + // Calculate the differences in x and y coordinates. + var dx = points[i - 1].X - points[i + 1].X; + var dy = points[i - 1].Y - points[i + 1].Y; + var dz = points[i - 1].Z - points[i + 1].Z; + + // Calculate the control point coordinates. + var controlPointX1 = points[i].X - dx * (1 / looseness); + var controlPointY1 = points[i].Y - dy * (1 / looseness); + var controlPointZ1 = points[i].Z - dz * (1 / looseness); + var controlPoint1 = new Vector3(controlPointX1, controlPointY1, controlPointZ1); + + var controlPointX2 = points[i].X + dx * (1 / looseness); + var controlPointY2 = points[i].Y + dy * (1 / looseness); + var controlPointZ2 = points[i].Z + dz * (1 / looseness); + var controlPoint2 = new Vector3(controlPointX2, controlPointY2, controlPointZ2); + + // Create an array to store the control points. + Vector3[] controlPointArray = new Vector3[] + { + controlPoint1, + controlPoint2 + }; + + // Add the control points to the list. + controlPoints.Add(controlPointArray); + } + + // Return the list of control points. + return controlPoints; + } + /// public override bool Intersects(ICurve curve, out List results) { @@ -459,7 +1050,7 @@ public bool Intersects(Ellipse ellipse, out List results) // Bezier curve always inside it's bounding box. // Rough check if curve is too far away. var boxCenter = box.Center(); - if (ellipse.Center.DistanceTo(boxCenter) > + if (ellipse.Center.DistanceTo(boxCenter) > Math.Max(ellipse.MajorAxis, ellipse.MinorAxis) + (box.Max - boxCenter).Length()) { results = new List(); @@ -509,7 +1100,7 @@ public bool Intersects(Bezier other, out List results) Intersects(other, (box, Domain), - (otherBox, other.Domain), + (otherBox, other.Domain), leftCache, rightCache, ref results); @@ -566,7 +1157,7 @@ private void Intersects(Bezier other, Intersects(other, left, loRight, leftCache, rightCache, ref results); Intersects(other, left, hiRight, leftCache, rightCache, ref results); } - else if (!rightSplit) + else if (!rightSplit) { Intersects(other, loLeft, right, leftCache, rightCache, ref results); Intersects(other, hiLeft, right, leftCache, rightCache, ref results); @@ -589,7 +1180,7 @@ private bool SplitCurveBox((BBox3 Box, Domain1d Domain) def, high = (default, default); // If curve bounding box is tolerance size - it's considered as intersection. - // Otherwise calculate new boxes of two halves of the curve. + // Otherwise calculate new boxes of two halves of the curve. var epsilon2 = Vector3.EPSILON * Vector3.EPSILON; var leftConvergent = (def.Box.Max - def.Box.Min).LengthSquared() < epsilon2 * 2; if (leftConvergent) @@ -598,7 +1189,7 @@ private bool SplitCurveBox((BBox3 Box, Domain1d Domain) def, } // If curve bounding box is tolerance size - it's considered as intersection. - // Otherwise calculate new boxes of two halves of the curve. + // Otherwise calculate new boxes of two halves of the curve. low = CurveBoxHalf(def, cache, true); high = CurveBoxHalf(def, cache, false); return true; diff --git a/Elements/src/Geometry/Circle.cs b/Elements/src/Geometry/Circle.cs index 69296293e..7a75056b8 100644 --- a/Elements/src/Geometry/Circle.cs +++ b/Elements/src/Geometry/Circle.cs @@ -1,13 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using Elements.Validators; using Elements.Geometry.Interfaces; using Newtonsoft.Json; namespace Elements.Geometry { /// - /// A circle. + /// A circle. /// Parameterization of the circle is 0 -> 2PI. /// public class Circle : Curve, IConic @@ -27,6 +28,11 @@ public Vector3 Center [System.ComponentModel.DataAnnotations.Range(0.0D, double.MaxValue)] public double Radius { get; protected set; } + /// The circumference of the circle. + [JsonIgnore] + [System.ComponentModel.DataAnnotations.Range(0.0D, double.MaxValue)] + public double Circumference { get; protected set; } + /// /// The coordinate system of the plane containing the circle. /// @@ -50,7 +56,15 @@ public Vector3 Normal [JsonConstructor] public Circle(Vector3 center, double radius = 1.0) { + if (!Validator.DisableValidationOnConstruction) + { + if (Math.Abs(radius - 0.0) < double.Epsilon ? true : false) + { + throw new ArgumentException($"The circle could not be created. The radius of the circle cannot be the zero: radius {radius}"); + } + } this.Radius = radius; + this.Circumference = 2 * Math.PI * this.Radius; this.Transform = new Transform(center); } @@ -60,7 +74,15 @@ public Circle(Vector3 center, double radius = 1.0) /// The radius of the circle. public Circle(double radius = 1.0) { + if (!Validator.DisableValidationOnConstruction) + { + if (Math.Abs(radius - 0.0) < double.Epsilon ? true : false) + { + throw new ArgumentException($"The circle could not be created. The radius of the circle cannot be the zero: radius {radius}"); + } + } this.Radius = radius; + this.Circumference = 2 * Math.PI * this.Radius; this.Transform = new Transform(); } @@ -69,8 +91,65 @@ public Circle(double radius = 1.0) /// public Circle(Transform transform, double radius = 1.0) { + if (!Validator.DisableValidationOnConstruction) + { + if (Math.Abs(radius - 0.0) < double.Epsilon ? true : false) + { + throw new ArgumentException($"The circle could not be created. The radius of the circle cannot be the zero: radius {radius}"); + } + } this.Transform = transform; this.Radius = radius; + this.Circumference = 2 * Math.PI * this.Radius; + } + + /// + /// Calculate the length of the circle between two parameters. + /// + public double ArcLength(double start, double end) + { + // Convert start and end parameters from radians to degrees + double _startAngle = start * 180.0 / Math.PI; + double _endAngle = end * 180.0 / Math.PI; + + // Ensure the start angle is within the valid domain range of 0 to 360 degrees + double startAngle = _startAngle % 360; + if (startAngle < 0) + { + startAngle += 360; + } + + // Ensure the end angle is within the valid domain range of 0 to 360 degrees + double endAngle = _endAngle % 360; + if (endAngle < 0) + { + endAngle += 360; + } + else if (endAngle == 0 && Math.Abs(_endAngle) >= 2 * Math.PI) + { + endAngle = 360; + } + + // Calculate the difference in angles + double angleDifference = endAngle - startAngle; + + // Adjust the angle difference if it crosses the 360-degree boundary + if (angleDifference < 0) + { + angleDifference += 360; + } + else if (angleDifference >= 2 * Math.PI) + { + return Circumference; // Full circle, return circumference + } + + // Convert the angle difference back to radians + double angleDifferenceRadians = angleDifference * Math.PI / 180.0; + + // Calculate the arc length using the formula: arc length = radius * angle + double arcLength = Radius * angleDifferenceRadians; + + return arcLength; } /// @@ -94,6 +173,14 @@ public Polygon ToPolygon(int divisions = 10) return new Polygon(pts, true); } + /// + /// Are the two circles almost equal? + /// + public bool IsAlmostEqualTo(Circle other, double tolerance = Vector3.EPSILON) + { + return (Center.IsAlmostEqualTo(other.Center, tolerance) && Math.Abs(Radius - other.Radius) < tolerance ? true : false); + } + /// /// Convert a circle to a circular arc. /// @@ -105,6 +192,15 @@ public Polygon ToPolygon(int divisions = 10) /// The bounded curve to convert. public static implicit operator ModelCurve(Circle c) => new ModelCurve(c); + /// + /// Calculates and returns the midpoint of the circle. + /// + /// The midpoint of the circle. + public Vector3 MidPoint() + { + return PointAt(Math.PI); + } + /// /// Return the point at parameter u on the arc. /// @@ -122,6 +218,46 @@ private Vector3 PointAtUntransformed(double u) return new Vector3(x, y); } + /// + /// Calculates and returns the point on the circle at a specific arc length. + /// + /// The arc length along the circumference of the circle. + /// The point on the circle at the specified arc length. + public Vector3 PointAtLength(double length) + { + double parameter = (length / Circumference) * 2 * Math.PI; + return PointAt(parameter); + } + + /// + /// Calculates and returns the point on the circle at a normalized arc length. + /// + /// The normalized arc length between 0 and 1. + /// The point on the circle at the specified normalized arc length. + public Vector3 PointAtNormalizedLength(double normalizedLength) + { + double parameter = normalizedLength * 2 * Math.PI; + return PointAt(parameter); + } + + /// + /// Calculates the parameter within the range of 0 to 2π at a given point on the circle. + /// + /// The point on the circle. + /// The parameter within the range of 0 to 2π at the given point on the circle. + public double GetParameterAt(Vector3 point) + { + Vector3 relativePoint = point - Center; + + double theta = Math.Atan2(relativePoint.Y, relativePoint.X); + + if (theta < 0) + { + theta += 2 * Math.PI; + } + return theta; + } + /// /// Check if certain point is on the circle. /// @@ -143,6 +279,22 @@ public bool ParameterAt(Vector3 pt, out double t) return false; } + /// + /// Checks if a given point lies on a circle within a specified tolerance. + /// + /// The point to be checked. + /// The circle to check against. + /// The tolerance value (optional). Default is 1E-05. + /// True if the point lies on the circle within the tolerance, otherwise false. + public static bool PointOnCircle(Vector3 point, Circle circle, double tolerance = 1E-05) + { + Vector3 centerToPoint = point - circle.Center; + double distanceToCenter = centerToPoint.Length(); + + // Check if the distance from the point to the center is within the tolerance of the circle's radius + return Math.Abs(distanceToCenter - circle.Radius) < tolerance; + } + private double ParameterAtUntransformed(Vector3 pt) { var v = pt / Radius; @@ -187,6 +339,26 @@ public override double ParameterAtDistanceFromParameter(double distance, double return start + theta; } + /// + /// Divides the circle into segments of the specified length and returns a list of points representing the division. + /// + /// The length of each segment. + /// A list of points representing the division of the circle. + public Vector3[] DivideByLength(double length) + { + List points = new List(); + int segmentCount = (int)Math.Ceiling(Circumference / length); + double segmentLength = Circumference / segmentCount; + + for (int i = 0; i < segmentCount; i++) + { + double parameter = i * segmentLength / Circumference; + points.Add(PointAtNormalizedLength(parameter)); + } + + return points.ToArray(); + } + /// public override bool Intersects(ICurve curve, out List results) { @@ -194,7 +366,7 @@ public override bool Intersects(ICurve curve, out List results) { case BoundedCurve boundedCurve: return boundedCurve.Intersects(this, out results); - case InfiniteLine line : + case InfiniteLine line: return Intersects(line, out results); case Circle circle: return Intersects(circle, out results); @@ -219,7 +391,7 @@ public bool Intersects(Circle other, out List results) Plane planeA = new Plane(Center, Normal); Plane planeB = new Plane(other.Center, other.Normal); - // Check if two circles are on the same plane. + // Check if two circles are on the same plane. if (Normal.IsParallelTo(other.Normal, Vector3.EPSILON * Vector3.EPSILON) && other.Center.DistanceTo(planeA).ApproximatelyEquals(0)) { diff --git a/Elements/src/Geometry/Interfaces/IHasArcLength.cs b/Elements/src/Geometry/Interfaces/IHasArcLength.cs new file mode 100644 index 000000000..fe8a3cc90 --- /dev/null +++ b/Elements/src/Geometry/Interfaces/IHasArcLength.cs @@ -0,0 +1,38 @@ +namespace Elements.Geometry.Interfaces +{ + /// + /// Represents a curve with arc length-based operations. + /// Implementing classes define methods for computing points and performing operations based on arc length. + /// + public interface IHasArcLength + { + /// + /// Returns the point on the curve at the specified arc length. + /// + /// The arc length along the curve. + /// The point on the curve at the specified arc length. + Vector3 PointAtLength(double length); + + /// + /// Returns the point on the curve at the specified normalized length. + /// The normalized length is a value between 0 and 1 representing the relative position along the curve. + /// + /// The normalized length along the curve. + /// The point on the curve at the specified normalized length. + Vector3 PointAtNormalizedLength(double normalizedLength); + + /// + /// Returns the midpoint of the curve. + /// + /// The midpoint of the curve. + Vector3 MidPoint(); + + /// + /// Divides the curve into segments of the specified length and returns the points along the curve at those intervals. + /// + /// The desired length for dividing the curve. + /// A list of points representing the divisions along the curve. + Vector3[] DivideByLength(double length); + } + +} \ No newline at end of file diff --git a/Elements/src/Geometry/Line.cs b/Elements/src/Geometry/Line.cs index 068292ef5..a96dca17e 100644 --- a/Elements/src/Geometry/Line.cs +++ b/Elements/src/Geometry/Line.cs @@ -123,6 +123,48 @@ public override Vector3 PointAt(double u) return this.BasisCurve.PointAt(u); } + /// + /// The mid point of the curve. + /// + /// The length based midpoint. + public virtual Vector3 MidPoint() + { + return PointAtNormalizedLength(0.5); + } + + /// + /// Returns the point on the line corresponding to the specified length value. + /// + /// The length value along the line. + /// The point on the line corresponding to the specified length value. + /// Thrown when the specified length is out of range. + public virtual Vector3 PointAtLength(double length) + { + double totalLength = ArcLength(this.Domain.Min, this.Domain.Max); // Calculate the total length of the Line + + if (length < 0 || length > totalLength) + { + throw new ArgumentException("The specified length is out of range."); + } + var lengthParameter = length / totalLength; + return this.PointAtNormalized(lengthParameter); + } + + /// + /// Returns the point on the line corresponding to the specified normalized length-based parameter value. + /// + /// The normalized length-based parameter value, ranging from 0 to 1. + /// The point on the line corresponding to the specified normalized length-based parameter value. + /// Thrown when the specified parameter is out of range. + public virtual Vector3 PointAtNormalizedLength(double parameter) + { + if (parameter < 0 || parameter > 1) + { + throw new ArgumentException("The specified parameter is out of range."); + } + return PointAtLength(parameter * this.ArcLength(this.Domain.Min, this.Domain.Max)); + } + /// public override Curve Transformed(Transform transform) { @@ -558,7 +600,7 @@ public static bool PointOnLine(Vector3 point, Vector3 start, Vector3 end, bool i /// /// The length. /// A flag indicating whether segments shorter than l should be removed. - public List DivideByLength(double l, bool removeShortSegments = false) + public List DivideByLengthToSegments(double l, bool removeShortSegments = false) { var len = this.Length(); if (l > len) @@ -587,7 +629,50 @@ public List DivideByLength(double l, bool removeShortSegments = false) return lines; } - /// + /// + /// Divides the line into segments of the specified length. + /// + /// The desired length of each segment. + /// A list of points representing the segments. + public Vector3[] DivideByLengthToPoints(double divisionLength) + { + var segments = new List(); + + if (this.ArcLength(this.Domain.Min, this.Domain.Max) < double.Epsilon) + { + // Handle invalid line with insufficient length + return new Vector3[0]; + } + + var currentProgression = 0.0; + segments = new List { this.Start }; + + // currentProgression from last segment before hitting end + if (currentProgression != 0.0) + { + currentProgression -= divisionLength; + } + while (this.ArcLength(this.Domain.Min, this.Domain.Max) >= currentProgression + divisionLength) + { + segments.Add(this.PointAt(currentProgression + divisionLength)); + currentProgression += divisionLength; + } + // Set currentProgression from divisionLength less distance from last segment point + currentProgression = divisionLength - segments.LastOrDefault().DistanceTo(this.End); + + // Add the last vertex of the polyline as the endpoint of the last segment if it + // is not already part of the list + if (!segments.LastOrDefault().IsAlmostEqualTo(this.End)) + { + segments.Add(this.End); + } + + return segments.ToArray(); + } + + /// + /// The mid point of the line. + /// public override Vector3 Mid() { return Start.Average(End); diff --git a/Elements/src/Geometry/Polyline.cs b/Elements/src/Geometry/Polyline.cs index b7922cdba..ece888b0d 100644 --- a/Elements/src/Geometry/Polyline.cs +++ b/Elements/src/Geometry/Polyline.cs @@ -84,6 +84,64 @@ public virtual Line[] Segments() return SegmentsInternal(this.Vertices); } + /// + /// The mid point of the curve. + /// + /// The length based midpoint. + public virtual Vector3 MidPoint() + { + return PointAtNormalizedLength(0.5); + } + + /// + /// Returns the point on the polyline corresponding to the specified length value. + /// + /// The length value along the polyline. + /// The point on the polyline corresponding to the specified length value. + /// Thrown when the specified length is out of range. + public virtual Vector3 PointAtLength(double length) + { + double totalLength = ArcLength(this.Domain.Min, this.Domain.Max); // Calculate the total length of the Polyline + if (length < 0 || length > totalLength) + { + throw new ArgumentException("The specified length is out of range."); + } + + double accumulatedLength = 0.0; + foreach (Line segment in Segments()) + { + double segmentLength = segment.ArcLength(segment.Domain.Min, segment.Domain.Max); + + if (accumulatedLength + segmentLength >= length) + { + double remainingDistance = length - accumulatedLength; + double parameter = remainingDistance / segmentLength; + return segment.PointAtNormalized(parameter); + } + + accumulatedLength += segmentLength; + } + + // If we reach here, the desired length is equal to the total length, + // so return the end point of the Polyline. + return End; + } + + /// + /// Returns the point on the polyline corresponding to the specified normalized length-based parameter value. + /// + /// The normalized length-based parameter value, ranging from 0 to 1. + /// The point on the polyline corresponding to the specified normalized length-based parameter value. + /// Thrown when the specified parameter is out of range. + public virtual Vector3 PointAtNormalizedLength(double parameter) + { + if (parameter < 0 || parameter > 1) + { + throw new ArgumentException("The specified parameter is out of range."); + } + return PointAtLength(parameter * this.ArcLength(this.Domain.Min, this.Domain.Max)); + } + /// /// Get the transform at the specified parameter along the polyline. /// @@ -289,7 +347,7 @@ public override Transform[] Frames(double startSetbackDistance = 0.0, // Calculate number of frames. 2 frames corresponding to end parameters. // 1 if startIndex == endIndex. - var length = endIndex - startIndex + 3; + var length = endIndex - startIndex + 3; // startIndex is set to the first distinct vertex after startParam. if (startParam.ApproximatelyEquals(startIndex)) @@ -316,7 +374,7 @@ public override Transform[] Frames(double startSetbackDistance = 0.0, result[0] = new Transform(PointAt(startParam), normals[startIndex - 1].Cross(tangent), tangent); index++; } - + for (var i = startIndex; i <= endIndex; i++, index++) { result[index] = CreateOrthogonalTransform(i, Vertices[i], normals[i]); @@ -408,6 +466,48 @@ private Transform CreateOrthogonalTransform(int i, Vector3 origin, Vector3 up) return new Transform(origin, up.Cross(tangent), tangent); } + /// + /// Divides the polyline into segments of the specified length. + /// + /// The desired length of each segment. + /// A list of points representing the segments. + public Vector3[] DivideByLength(double divisionLength) + { + if (this.Vertices.Count < 2) + { + // Handle invalid polyline with insufficient vertices + return new Vector3[0]; + } + + var currentProgression = 0.0; + var segments = new List { this.Vertices.FirstOrDefault() }; + + foreach (var currentSegment in this.Segments()) + { + // currentProgression from last segment before hitting end + if (currentProgression != 0.0) + { + currentProgression -= divisionLength; + } + while (currentSegment.ArcLength(currentSegment.Domain.Min, currentSegment.Domain.Max) >= currentProgression + divisionLength) + { + segments.Add(currentSegment.PointAt(currentProgression + divisionLength)); + currentProgression += divisionLength; + } + // Set currentProgression from divisionLength less distance from last segment point + currentProgression = divisionLength - segments.LastOrDefault().DistanceTo(currentSegment.End); + } + + // Add the last vertex of the polyline as the endpoint of the last segment if it + // is not already part of the list + if (!segments.LastOrDefault().IsAlmostEqualTo(this.Vertices.LastOrDefault())) + { + segments.Add(this.Vertices.LastOrDefault()); + } + + return segments.ToArray(); + } + /// /// Offset this polyline by the specified amount. /// @@ -817,7 +917,7 @@ protected void Split(IList points, bool closed = false) var b = closed && i == this.Vertices.Count - 1 ? this.Vertices[0] : this.Vertices[i + 1]; var edge = (a, b); - // An edge may have multiple split points. + // An edge may have multiple split points. // We store these in a list and sort it along the // direction of the edge, before inserting the points // into the vertex list and incrementing i by the correct @@ -1250,4 +1350,4 @@ public IndexedPolycurve Fillet(double radius) return new IndexedPolycurve(curves); } } -} +} \ No newline at end of file diff --git a/Elements/test/BezierTests.cs b/Elements/test/BezierTests.cs index 2df434b0e..aafdb9584 100644 --- a/Elements/test/BezierTests.cs +++ b/Elements/test/BezierTests.cs @@ -92,6 +92,167 @@ public void IntersectsLine() Assert.False(bezier.Intersects(line, out results)); } + [Fact] + public void Bezier_ArcLength() + { + var a = new Vector3(50, 150, 0); + var b = new Vector3(105, 66, 0); + var c = new Vector3(170, 230, 0); + var d = new Vector3(200, 150, 0); + var ctrlPts = new List { a, b, c, d }; + var bezier = new Bezier(ctrlPts); + + var expectedLength = 184.38886379602502; // approximation as the integral function used for calculating length is not 100% accurate + Assert.Equal(expectedLength, bezier.ArcLength(0.0, 1.0), 2); + } + + [Fact] + public void GetParameterAt() + { + var tolerance = 0.00001; + + var a = new Vector3(1, 5, 0); + var b = new Vector3(5, 20, 0); + var c = new Vector3(5, -10, 0); + var d = new Vector3(9, 5, 0); + var ctrlPts = new List { a, b, c, d }; + var bezier = new Bezier(ctrlPts); + + var samplePt = new Vector3(0, 0, 0); + Assert.Null(bezier.ParameterAt(samplePt, tolerance)); + + samplePt = new Vector3(6.625, 0.7812, 0.0); + Assert.True((double)bezier.ParameterAt(samplePt, tolerance) - 0.75 <= tolerance * 10); + } + + [Fact] + public void GetPointAt() + { + var a = new Vector3(1, 5, 0); + var b = new Vector3(5, 20, 0); + var c = new Vector3(5, -10, 0); + var d = new Vector3(9, 5, 0); + var ctrlPts = new List { a, b, c, d }; + var bezier = new Bezier(ctrlPts); + + var testPt = new Vector3(3.3750, 9.21875, 0.0000); + Assert.True(testPt.Equals(bezier.PointAt(0.25))); + + testPt = new Vector3(4.699, 6.11375, 0.0000); + Assert.True(testPt.Equals(bezier.PointAtNormalized(0.45))); + + testPt = new Vector3(4.515904, 6.75392, 0.0000); + Assert.True(testPt.Equals(bezier.PointAtLength(8.0))); + + testPt = new Vector3(3.048823, 9.329262, 0.0000); + Assert.True(testPt.Equals(bezier.PointAtNormalizedLength(0.25))); + } + + [Fact] + public void DivideByLength() + { + var a = new Vector3(50, 150, 0); + var b = new Vector3(105, 66, 0); + var c = new Vector3(170, 230, 0); + var d = new Vector3(200, 150, 0); + var ctrlPts = new List { a, b, c, d }; + var bezier = new Bezier(ctrlPts); + + var testPts = new List(){ + new Vector3(50.00, 150.00, 0.00), + new Vector3(90.705919, 125.572992, 0.00), + new Vector3(134.607122, 148.677130, 0.00), + new Vector3(177.600231, 172.675064, 0.00), + new Vector3(200.00, 150.00, 0.00), + }; + + var ptsFromBezier = bezier.DivideByLength(50.0); + + for (int i = 0; i < ptsFromBezier.Length; i++) + { + Assert.True(ptsFromBezier[i].Equals(testPts[i])); + } + } + + [Fact] + public void Split() + { + var a = new Vector3(50, 150, 0); + var b = new Vector3(105, 66, 0); + var c = new Vector3(170, 230, 0); + var d = new Vector3(200, 150, 0); + var ctrlPts = new List { a, b, c, d }; + var bezier = new Bezier(ctrlPts); + + var testBeziers = new List(){ + new Bezier(new List() { + new Vector3(50.00, 150.00, 0.00), + new Vector3(63.75, 129.00, 0.00), + new Vector3(78.1250, 123.50, 0.00), + new Vector3(92.421875, 125.8125, 0.00) + } + ), + new Bezier(new List() { + new Vector3(92.421875, 125.8125, 0.00), + new Vector3(121.015625, 130.4375, 0.00), + new Vector3(149.296875, 166.3125, 0.00), + new Vector3(171.640625, 171.9375, 0.00) + } + ), + new Bezier(new List() { + new Vector3(171.640625, 171.9375, 0.00), + new Vector3(182.8125, 174.7500, 0.00), + new Vector3(192.50, 170.00, 0.00), + new Vector3(200.00, 150.00, 0.00) + } + ), + }; + + var beziers = bezier.Split(new List() { 0.25, 0.75 }); + + for (int i = 0; i < beziers.Count; i++) + { + Assert.True(beziers[i].ControlPoints[0].Equals(testBeziers[i].ControlPoints[0])); + Assert.True(beziers[i].ControlPoints[1].Equals(testBeziers[i].ControlPoints[1])); + Assert.True(beziers[i].ControlPoints[2].Equals(testBeziers[i].ControlPoints[2])); + Assert.True(beziers[i].ControlPoints[3].Equals(testBeziers[i].ControlPoints[3])); + } + + testBeziers = new List(){ + new Bezier(new List() { + new Vector3(50.00, 150.00, 0.00), + new Vector3(61.88, 131.856, 0.00), + new Vector3(74.22656, 125.282688, 0.00), + new Vector3(86.586183, 125.321837, 0.00) + } + ), + new Bezier(new List() { + new Vector3(86.586183, 125.321837, 0.00), + new Vector3(114.738659, 125.411011, 0.00), + new Vector3(142.958913, 159.807432, 0.00), + new Vector3(165.887648, 169.916119, 0.00) + } + ), + new Bezier(new List() { + new Vector3(165.887648, 169.916119, 0.00), + new Vector3(179.49576, 175.915584, 0.00), + new Vector3(191.24, 173.36, 0.00), + new Vector3(200.00, 150.00, 0.00) + } + ), + }; + + var normalizedBeziers = bezier.Split(new List() { 0.25, 0.75 }, true); + + for (int i = 0; i < normalizedBeziers.Count; i++) + { + Assert.True(normalizedBeziers[i].ControlPoints[0].Equals(testBeziers[i].ControlPoints[0])); + Assert.True(normalizedBeziers[i].ControlPoints[1].Equals(testBeziers[i].ControlPoints[1])); + Assert.True(normalizedBeziers[i].ControlPoints[2].Equals(testBeziers[i].ControlPoints[2])); + Assert.True(normalizedBeziers[i].ControlPoints[3].Equals(testBeziers[i].ControlPoints[3])); + } + } + [Fact] public void IntersectsCircle() { @@ -147,8 +308,8 @@ public void IntersectsPolycurve() var polygon = new Polygon(new Vector3[]{ (0, 3), (6, 3), (4, 1), (-2, 1) - }); - + }); + Assert.True(bezier.Intersects(polygon, out var results)); Assert.Equal(4, results.Count); Assert.Contains(new Vector3(0.93475, 3), results); diff --git a/Elements/test/CircleTests.cs b/Elements/test/CircleTests.cs index 5be213dde..75a9331cf 100644 --- a/Elements/test/CircleTests.cs +++ b/Elements/test/CircleTests.cs @@ -1,20 +1,114 @@ -using Elements; -using Elements.Geometry; -using Elements.Tests; using System; +using System.Collections.Generic; +using System.IO; using System.Linq; -using System.Security.Cryptography; +using Elements.Tests; +using Newtonsoft.Json; using Xunit; -using Xunit.Abstractions; namespace Elements.Geometry.Tests { - public class CircleTests + public class CircleTests : ModelTest { + public CircleTests() + { + this.GenerateIfc = false; + } + + [Fact, Trait("Category", "Examples")] + public void CircleExample() + { + this.Name = "Elements_Geometry_Circle"; + + // + var a = new Vector3(); + var b = 1.0; + var c = new Circle(a, b); + // + + this.Model.AddElement(c); + } + + [Fact] + public void Equality() + { + var p = 1.0; + var circleA = new Circle(Vector3.Origin, p); + var circleB = new Circle(Vector3.Origin, p + 1E-4); + var circleC = new Circle(Vector3.Origin, p + 1E-6); + + Assert.False(circleA.IsAlmostEqualTo(circleB)); + Assert.True(circleA.IsAlmostEqualTo(circleB, 1E-3)); + Assert.True(circleA.IsAlmostEqualTo(circleC)); + } + + [Fact] + public void Construct() + { + var a = new Vector3(); + var b = 1.0; + var c = new Circle(a, b); + Assert.Equal(1.0, c.Radius); + Assert.Equal(new Vector3(0, 0), c.Center); + Assert.Equal(a + new Vector3(b, 0, 0), c.PointAt(-1e-10)); + } + + [Fact] + public void ZeroRadius_ThrowsException() + { + var a = new Vector3(); + Assert.Throws(() => new Circle(a, 0)); + } + + [Fact] + public void GetParameterAt() + { + var center = Vector3.Origin; + var radius = 5.0; + var circle = new Circle(center, radius); + var start = new Vector3(5.0, 0.0, 0.0); + var mid = new Vector3(-5.0, 0.0, 0.0); + + Assert.Equal(0, circle.GetParameterAt(start)); + + var almostEqualStart = new Vector3(5.000001, 0.000005, 0); + Assert.True(start.IsAlmostEqualTo(almostEqualStart)); + + Assert.True(Math.Abs(circle.GetParameterAt(mid) - Math.PI) < double.Epsilon ? true : false); + + Assert.Equal(circle.Circumference, 2 * Math.PI * radius); + var vector = new Vector3(3.535533, 3.535533, 0.0); + var uValue = circle.GetParameterAt(vector); + var expectedVector = circle.PointAt(uValue); + Assert.InRange(uValue, 0, circle.Circumference); + Assert.True(vector.IsAlmostEqualTo(expectedVector)); + + var parameter = 0.5; + var testParameterMidpoint = circle.PointAtNormalizedLength(parameter); + Assert.True(testParameterMidpoint.IsAlmostEqualTo(mid)); + + var midlength = circle.Circumference * parameter; + var testLengthMidpoint = circle.PointAtLength(midlength); + Assert.True(testLengthMidpoint.IsAlmostEqualTo(testParameterMidpoint)); + + var midpoint = circle.MidPoint(); + Assert.True(midpoint.IsAlmostEqualTo(testLengthMidpoint)); + } + + [Fact] + public void PointOnCircle() + { + Circle circle = new Circle(Vector3.Origin, 5.0); + + Assert.False(Circle.PointOnCircle(Vector3.Origin, circle)); + Assert.False(Circle.PointOnCircle(new Vector3(4, 0, 0), circle)); + Assert.True(Circle.PointOnCircle(circle.PointAtNormalizedLength(0.5), circle)); + } + [Fact] public void CirceIntersectsCircle() { - // Planar intersecting circles + // Planar intersecting circles Circle c0 = new Circle(5); Circle c1 = new Circle(new Vector3(8, 0, 0), 5); Assert.True(c0.Intersects(c1, out var results)); diff --git a/Elements/test/LineTests.cs b/Elements/test/LineTests.cs index 507c7b61c..1daf43de8 100644 --- a/Elements/test/LineTests.cs +++ b/Elements/test/LineTests.cs @@ -141,7 +141,7 @@ public void IntersectsQuick() public void IntersectsCircle() { Circle c = new Circle(new Vector3(5, 5, 5), 5); - + // Intersects circle at one point and touches at other. Line l = new Line(new Vector3(0, 5, 5), new Vector3(15, 5, 5)); Assert.True(l.Intersects(c, out var results)); @@ -259,14 +259,25 @@ public void DivideIntoEqualSegmentsSingle() } [Fact] - public void DivideByLength() + public void DivideByLengthToPoints() { var l = new Line(Vector3.Origin, new Vector3(5, 0)); - var segments = l.DivideByLength(1.1, true); - Assert.Equal(4, segments.Count); + var segments = l.DivideByLengthToPoints(1.1); + Assert.Equal(6, segments.Count()); - var segments1 = l.DivideByLength(1.1); - Assert.Equal(5, segments1.Count); + var segments1 = l.DivideByLengthToPoints(2); + Assert.Equal(4, segments1.Count()); + } + + [Fact] + public void DivideByLengthToPSegments() + { + var l = new Line(Vector3.Origin, new Vector3(5, 0)); + var segments = l.DivideByLengthToSegments(1.1); + Assert.Equal(5, segments.Count()); + + var segments1 = l.DivideByLengthToSegments(2); + Assert.Equal(3, segments1.Count()); } [Fact] @@ -717,7 +728,6 @@ public void TryGetOverlap() Assert.Equal(new Line((5, 0, 0), (10, 0, 0)), overlap); } - [Fact] public void GetParameterAt() { @@ -749,6 +759,17 @@ public void GetParameterAt() var expectedVector = line.PointAt(uValue); Assert.InRange(uValue, 0, line.Length()); Assert.True(vector.IsAlmostEqualTo(expectedVector)); + + var parameter = 0.5; + var testParameterMidpoint = line.PointAtNormalizedLength(parameter); + Assert.True(testParameterMidpoint.IsAlmostEqualTo(middle)); + + var midlength = line.Length() * parameter; + var testLengthMidpoint = line.PointAtLength(midlength); + Assert.True(testLengthMidpoint.IsAlmostEqualTo(testParameterMidpoint)); + + var midpoint = line.MidPoint(); + Assert.True(midpoint.IsAlmostEqualTo(testLengthMidpoint)); } [Theory] @@ -1209,7 +1230,7 @@ public void LineDistancePointsOnSkewLines() Assert.Equal(delta.Length(), (new Line(pt12, pt11)).DistanceTo(new Line(pt21, pt22)), 12); Assert.Equal(delta.Length(), (new Line(pt12, pt11)).DistanceTo(new Line(pt22, pt21)), 12); //The segments (pt12, pt13) and (pt21, pt22) does not intersect. - //The shortest distance is from an endpoint to another segment - difference between lines plus between endpoints. + //The shortest distance is from an endpoint to another segment - difference between lines plus between endpoints. var expected = (q12 * v1).DistanceTo(new Line(delta + q21 * v2, delta + q22 * v2)); Assert.Equal(expected, (new Line(pt12, pt13)).DistanceTo(new Line(pt21, pt22)), 12); Assert.Equal(expected, (new Line(pt12, pt13)).DistanceTo(new Line(pt22, pt21)), 12); diff --git a/Elements/test/PolylineTests.cs b/Elements/test/PolylineTests.cs index 010221e7e..2511aa79f 100644 --- a/Elements/test/PolylineTests.cs +++ b/Elements/test/PolylineTests.cs @@ -257,6 +257,76 @@ public void GetParameterAt() Assert.Equal(1.5, resultParameter); var testPoint = polyline.PointAt(resultParameter); Assert.True(point.IsAlmostEqualTo(testPoint)); + + polyline = new Polyline( + new List() + { + new Vector3(1, 5, 0), + new Vector3(5, 20, 0), + new Vector3(5, 0, 0), + new Vector3(9, 5, 0), + } + ); + var parameter = 0.5; + var testMidpoint = new Vector3(5.0, 14.560525, 0); // Midpoint + var testParameterMidpoint = polyline.PointAtNormalizedLength(parameter); + Assert.True(testParameterMidpoint.IsAlmostEqualTo(testMidpoint)); + + var midlength = polyline.Length() * parameter; + var testLengthMidpoint = polyline.PointAtLength(midlength); + Assert.True(testLengthMidpoint.IsAlmostEqualTo(testParameterMidpoint)); + + var midpoint = polyline.MidPoint(); + Assert.True(midpoint.IsAlmostEqualTo(testLengthMidpoint)); + } + + [Fact] + public void DivideByLength_SingleSegment() + { + // Arrange + var start = new Vector3(1, 2); + var end = new Vector3(1, 4); + var polyline = new Polyline(new[] { start, end }); + var divisionLength = 1.0; + + // Act + var segments = polyline.DivideByLength(divisionLength); + + // Assert + Assert.Equal(3, segments.Count()); + Assert.Equal(start, segments[0]); + Assert.Equal(new Vector3(1, 3), segments[1]); + Assert.Equal(end, segments[2]); + } + + [Fact] + public void DivideByLength_MultipleSegments() + { + // Arrange + var polyline = new Polyline(new List() + { + new Vector3(1, 5, 0), + new Vector3(5, 20, 0), + new Vector3(5, 0, 0), + new Vector3(9, 5, 0), + }); + var divisionLength = 5.0; + + // Act + var segments = polyline.DivideByLength(divisionLength); + + // Assert + Assert.Equal(10, segments.Count()); + Assert.Equal(new Vector3(1.0, 5.0, 0), segments[0]); + Assert.Equal(new Vector3(2.288313, 9.831174, 0), segments[1]); + Assert.Equal(new Vector3(3.576626, 14.662349, 0), segments[2]); + Assert.Equal(new Vector3(4.864939, 19.493524, 0), segments[3]); + Assert.Equal(new Vector3(5.0, 15.524174, 0), segments[4]); + Assert.Equal(new Vector3(5.0, 10.524174, 0), segments[5]); + Assert.Equal(new Vector3(5.0, 5.524174, 0), segments[6]); + Assert.Equal(new Vector3(5.0, 0.524174, 0), segments[7]); + Assert.Equal(new Vector3(7.796025, 3.495032, 0), segments[8]); + Assert.Equal(new Vector3(9, 5, 0), segments[9]); } [Fact]