Skip to content

Commit

Permalink
Line implements Interface IHasArcLength
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesbradleym committed Jul 18, 2023
1 parent 6952333 commit 7309fc7
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 35 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
- `Arc` is now parameterized 0->2Pi
- `Line` now inherits from `TrimmedCurve<InfiniteLine>`.
- `Line` is now parameterized 0->length.
- `Line` now implements the `IHasArcLength` interface
- `Bezier` now inherits from `BoundedCurve`.
- `Polyline` is now parameterized 0->length.
- `Circle` is now parameterized 0->2Pi.
Expand Down
2 changes: 1 addition & 1 deletion Elements/src/Elements.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<Summary>The Elements library provides object types for generating the built environment.</Summary>
<PackageProjectUrl>https://github.com/hypar-io/elements</PackageProjectUrl>
<RepositoryUrl>https://github.com/hypar-io/elements</RepositoryUrl>
<Version>$(Version)</Version>
<Version>21.21.21</Version>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
<ItemGroup>
Expand Down
136 changes: 110 additions & 26 deletions Elements/src/Geometry/Line.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Linq;
using Newtonsoft.Json;
using Elements.Spatial;
using Elements.Geometry.Interfaces;

namespace Elements.Geometry
{
Expand All @@ -15,7 +16,7 @@ namespace Elements.Geometry
/// [!code-csharp[Main](../../Elements/test/LineTests.cs?name=example)]
/// </example>
/// TODO: Rename this class to LineSegment
public class Line : TrimmedCurve<InfiniteLine>, IEquatable<Line>
public class Line : TrimmedCurve<InfiniteLine>, IEquatable<Line>, IHasArcLength
{
/// <summary>
/// The domain of the curve.
Expand Down Expand Up @@ -123,6 +124,48 @@ public override Vector3 PointAt(double u)
return this.BasisCurve.PointAt(u);
}

/// <summary>
/// The mid point of the curve.
/// </summary>
/// <returns>The length based midpoint.</returns>
public virtual Vector3 MidPoint()
{
return PointAtNormalizedLength(0.5);
}

/// <summary>
/// Returns the point on the line corresponding to the specified length value.
/// </summary>
/// <param name="length">The length value along the line.</param>
/// <returns>The point on the line corresponding to the specified length value.</returns>
/// <exception cref="ArgumentException">Thrown when the specified length is out of range.</exception>
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);
}

/// <summary>
/// Returns the point on the line corresponding to the specified normalized length-based parameter value.
/// </summary>
/// <param name="parameter">The normalized length-based parameter value, ranging from 0 to 1.</param>
/// <returns>The point on the line corresponding to the specified normalized length-based parameter value.</returns>
/// <exception cref="ArgumentException">Thrown when the specified parameter is out of range.</exception>
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));
}

/// <inheritdoc/>
public override Curve Transformed(Transform transform)
{
Expand Down Expand Up @@ -541,38 +584,79 @@ public static bool PointOnLine(Vector3 point, Vector3 start, Vector3 end, bool i
return false;
}

// /// <summary>
// /// Divide the line into as many segments of the provided length as possible.
// /// </summary>
// /// <param name="l">The length.</param>
// /// <param name="removeShortSegments">A flag indicating whether segments shorter than l should be removed.</param>
// public List<Line> DivideByLength(double l, bool removeShortSegments = false)
// {
// var len = this.Length();
// if (l > len)
// {
// return new List<Line>() { new Line(this.Start, this.End) };
// }

// var total = 0.0;
// var d = this.Direction();
// var lines = new List<Line>();
// while (total + l <= len)
// {
// var a = this.Start + d * total;
// var b = a + d * l;
// lines.Add(new Line(a, b));
// total += l;
// }
// if (total < len && !removeShortSegments)
// {
// var a = this.Start + d * total;
// if (!a.IsAlmostEqualTo(End))
// {
// lines.Add(new Line(a, End));
// }
// }
// return lines;
// }

/// <summary>
/// Divide the line into as many segments of the provided length as possible.
/// Divides the line into segments of the specified length.
/// </summary>
/// <param name="l">The length.</param>
/// <param name="removeShortSegments">A flag indicating whether segments shorter than l should be removed.</param>
public List<Line> DivideByLength(double l, bool removeShortSegments = false)
/// <param name="divisionLength">The desired length of each segment.</param>
/// <returns>A list of points representing the segments.</returns>
public Vector3[] DivideByLength(double divisionLength)
{
var len = this.Length();
if (l > len)
var segments = new List<Vector3>();

if (this.ArcLength(this.Domain.Min, this.Domain.Max) < double.Epsilon)
{
return new List<Line>() { new Line(this.Start, this.End) };
// Handle invalid line with insufficient length
return new Vector3[0];
}

var total = 0.0;
var d = this.Direction();
var lines = new List<Line>();
while (total + l <= len)
var currentProgression = 0.0;
segments = new List<Vector3> { this.Start };

// currentProgression from last segment before hitting end
if (currentProgression != 0.0)
{
var a = this.Start + d * total;
var b = a + d * l;
lines.Add(new Line(a, b));
total += l;
currentProgression -= divisionLength;
}
if (total < len && !removeShortSegments)
while (this.ArcLength(this.Domain.Min, this.Domain.Max) >= currentProgression + divisionLength)
{
var a = this.Start + d * total;
if (!a.IsAlmostEqualTo(End))
{
lines.Add(new Line(a, End));
}
segments.Add(this.PointAt(currentProgression + divisionLength));
currentProgression += divisionLength;
}
return lines;
// 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();
}

/// <summary>
Expand Down Expand Up @@ -925,7 +1009,7 @@ public double DistanceTo(Line other)
// line vectors are not collinear, their directions share the common plane.
else
{
// dStartStart length is distance to the common plane.
// dStartStart length is distance to the common plane.
dStartStart = dStartStart.ProjectOnto(cross);
Vector3 vStartStart = other.Start + dStartStart - this.Start;
Vector3 vStartEnd = other.Start + dStartStart - this.End;
Expand Down Expand Up @@ -1028,8 +1112,8 @@ public List<Line> Trim(Polygon polygon, out List<Line> outsideSegments, bool inc
var B = intersectionsOrdered[i + 1];
if (A.IsAlmostEqualTo(B)) // skip duplicate points
{
// it's possible that A is outside, but B is at an edge, even
// if they are within tolerance of each other.
// it's possible that A is outside, but B is at an edge, even
// if they are within tolerance of each other.
// This can happen due to floating point error when the point is almost exactly
// epsilon distance from the edge.
// so if we have duplicate points, we have to update the containment value.
Expand Down
2 changes: 0 additions & 2 deletions Elements/src/Geometry/Polyline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,6 @@ public virtual Line[] Segments()
return SegmentsInternal(this.Vertices);
}



/// <summary>
/// The mid point of the curve.
/// </summary>
Expand Down
21 changes: 16 additions & 5 deletions Elements/test/LineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,11 @@ public void DivideIntoEqualSegmentsSingle()
public void DivideByLength()
{
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.DivideByLength(1.1);
Assert.Equal(6, segments.Count());

var segments1 = l.DivideByLength(1.1);
Assert.Equal(5, segments1.Count);
var segments1 = l.DivideByLength(2);
Assert.Equal(4, segments1.Count());
}

[Fact]
Expand Down Expand Up @@ -650,6 +650,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]
Expand Down Expand Up @@ -1110,7 +1121,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);
Expand Down
1 change: 0 additions & 1 deletion Elements/test/PolylineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,6 @@ public void GetParameterAt()
var parameter = 0.5;
var testMidpoint = new Vector3(5.0, 14.560525, 0); // Midpoint
var testParameterMidpoint = polyline.PointAtNormalizedLength(parameter);
var t = testParameterMidpoint.IsAlmostEqualTo(testMidpoint);
Assert.True(testParameterMidpoint.IsAlmostEqualTo(testMidpoint));

var midlength = polyline.Length() * parameter;
Expand Down

1 comment on commit 7309fc7

@jamesbradleym
Copy link
Contributor Author

@jamesbradleym jamesbradleym commented on 7309fc7 Jul 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open for discussion: Behavior change of DivideByLength, return Vector3[] rather than List.

It seems more valuable to me to receive back a list of the points that represent the division and allow someone to determine how they would like to use those points, such as placing instances across points, rather than sending back a list of lines that presumes they would want each Line which could quickly be constructed from these points.

Precedent for returning Points:
Rhino: Divide
AutoCAD: DIVIDE

Please sign in to comment.