Files
OpenNest/OpenNest.Core/Geometry/Line.cs
AJ Isaacs 29c2872819 fix(geometry): add Entity.Clone() and stop NormalizeEntities from mutating originals
ShapeProfile.NormalizeEntities called Shape.Reverse() which flipped arc
directions on the original entity objects shared with the CAD view. Switching
to the Program tab and back would leave arcs reversed. Clone entities before
normalizing so the originals stay untouched.

Adds abstract Entity.Clone() with implementations on Line, Arc, Circle,
Polygon, and Shape (deep-clones children). Also adds CloneAll() extension
and replaces manual duplication in PartGeometry.CopyEntitiesAtLocation and
ProgramEditorControl.CloneEntity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:35:13 -04:00

580 lines
17 KiB
C#

using OpenNest.Math;
using System;
using System.Collections.Generic;
namespace OpenNest.Geometry
{
public class Line : Entity
{
internal Vector pt1;
internal Vector pt2;
public Line()
{
}
public Line(double x1, double y1, double x2, double y2)
: this(new Vector(x1, y1), new Vector(x2, y2))
{
}
public Line(Vector startPoint, Vector endPoint)
{
pt1 = startPoint;
pt2 = endPoint;
UpdateBounds();
}
/// <summary>
/// Start point of the line.
/// </summary>
public Vector StartPoint
{
get { return pt1; }
set
{
pt1 = value;
UpdateBounds();
}
}
/// <summary>
/// Mid-point of the line.
/// </summary>
public Vector MidPoint
{
get
{
var x = (pt1.X + pt2.X) * 0.5;
var y = (pt1.Y + pt2.Y) * 0.5;
return new Vector(x, y);
}
}
/// <summary>
/// End point of the line.
/// </summary>
public Vector EndPoint
{
get { return pt2; }
set
{
pt2 = value;
UpdateBounds();
}
}
/// <summary>
/// Gets the point on the line that is perpendicular from the given point.
/// </summary>
/// <param name="pt"></param>
/// <returns></returns>
public Vector PointPerpendicularFrom(Vector pt)
{
var diff1 = pt - StartPoint;
var diff2 = EndPoint - StartPoint;
var dotProduct = diff1.X * diff2.X + diff1.Y * diff2.Y;
var lengthSquared = diff2.X * diff2.X + diff2.Y * diff2.Y;
var param = dotProduct / lengthSquared;
if (param < 0)
return StartPoint;
else if (param > 1)
return EndPoint;
else
{
return new Vector(
StartPoint.X + param * diff2.X,
StartPoint.Y + param * diff2.Y);
}
}
/// <summary>
/// Returns true if the given line is parallel to this.
/// </summary>
/// <param name="line"></param>
/// <returns></returns>
public bool IsParallelTo(Line line)
{
bool line1Vertical = IsVertical();
bool line2Vertical = line.IsVertical();
if (line1Vertical)
return line2Vertical;
else if (line2Vertical)
return false;
return Slope().IsEqualTo(line.Slope());
}
/// <summary>
/// Returns true if the given line is perpendicular to this.
/// </summary>
/// <param name="line"></param>
/// <returns></returns>
public bool IsPerpendicularTo(Line line)
{
bool line1Vertical = IsVertical();
bool line2Vertical = line.IsVertical();
if (line1Vertical)
return line.IsHorizontal();
else if (line.IsVertical())
return IsHorizontal();
return Slope().IsEqualTo(-1 / line.Slope());
}
/// <summary>
/// Returns true if the given line is intersecting this.
/// </summary>
/// <param name="line"></param>
/// <param name="pt">Point of intersection.</param>
/// <returns></returns>
public bool Intersects(Line line, out Vector pt)
{
var a1 = EndPoint.Y - StartPoint.Y;
var b1 = StartPoint.X - EndPoint.X;
var c1 = a1 * StartPoint.X + b1 * StartPoint.Y;
var a2 = line.EndPoint.Y - line.StartPoint.Y;
var b2 = line.StartPoint.X - line.EndPoint.X;
var c2 = a2 * line.StartPoint.X + b2 * line.StartPoint.Y;
var d = a1 * b2 - a2 * b1;
if (d.IsEqualTo(0.0))
{
pt = new Vector();
return false;
}
else
{
var x = (b2 * c1 - b1 * c2) / d;
var y = (a1 * c2 - a2 * c1) / d;
pt = new Vector(x, y);
return boundingBox.Contains(pt) && line.boundingBox.Contains(pt);
}
}
/// <summary>
/// Returns true if this is vertical.
/// </summary>
/// <returns></returns>
public bool IsVertical()
{
return pt1.X.IsEqualTo(pt2.X);
}
/// <summary>
/// Returns true if this is horizontal.
/// </summary>
/// <returns></returns>
public bool IsHorizontal()
{
return pt1.Y.IsEqualTo(pt2.Y);
}
/// <summary>
/// Returns true if the given line is collinear to this.
/// </summary>
/// <param name="line"></param>
/// <returns></returns>
public bool IsCollinearTo(Line line)
{
if (IsVertical())
{
if (!line.IsVertical())
return false;
return StartPoint.X.IsEqualTo(line.StartPoint.X);
}
else if (line.IsVertical())
return false;
if (!YIntercept().IsEqualTo(line.YIntercept()))
return false;
if (!Slope().IsEqualTo(line.Slope()))
return false;
return true;
}
/// <summary>
/// Angle of the line from start point to end point.
/// </summary>
/// <returns></returns>
public double Angle()
{
return StartPoint.AngleTo(EndPoint);
}
/// <summary>
/// Returns the angle between the two lines in radians.
/// </summary>
/// <param name="line"></param>
/// <returns></returns>
public double AngleBetween(Line line)
{
var m1 = Slope();
var m2 = line.Slope();
return System.Math.Atan(System.Math.Abs((m2 - m1) / (1 + m2 * m1)));
}
/// <summary>
/// Slope of the line.
/// </summary>
/// <returns></returns>
public double Slope()
{
if (IsVertical())
throw new DivideByZeroException();
return (EndPoint.Y - StartPoint.Y) / (EndPoint.X - StartPoint.X);
}
/// <summary>
/// Gets the y-axis intersection coordinate.
/// </summary>
/// <returns></returns>
public double YIntercept()
{
return StartPoint.Y - Slope() * StartPoint.X;
}
/// <summary>
/// Length of the line from start point to end point.
/// </summary>
public override double Length
{
get
{
var x = EndPoint.X - StartPoint.X;
var y = EndPoint.Y - StartPoint.Y;
return System.Math.Sqrt(x * x + y * y);
}
}
public override Entity Clone()
{
var copy = new Line(pt1, pt2);
CopyBaseTo(copy);
return copy;
}
/// <summary>
/// Reversed the line.
/// </summary>
public override void Reverse()
{
Generic.Swap<Vector>(ref pt1, ref pt2);
}
/// <summary>
/// Moves the start point to the given coordinates.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
public override void MoveTo(double x, double y)
{
var xoffset = pt1.X - x;
var yoffset = pt1.Y - y;
pt2.X += xoffset;
pt2.Y += yoffset;
pt1.X = x;
pt1.Y = y;
boundingBox.Offset(xoffset, yoffset);
}
/// <summary>
/// Moves the start point to the given point.
/// </summary>
/// <param name="pt"></param>
public override void MoveTo(Vector pt)
{
var offset = pt1 - pt;
pt2 += offset;
pt1 = pt;
boundingBox.Offset(offset);
}
/// <summary>
/// Offsets the line location by the given distances.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
public override void Offset(double x, double y)
{
pt2.X += x;
pt2.Y += y;
pt1.X += x;
pt1.Y += y;
boundingBox.Offset(x, y);
}
/// <summary>
/// Offsets the line location by the given distances.
/// </summary>
/// <param name="voffset"></param>
public override void Offset(Vector voffset)
{
pt1 += voffset;
pt2 += voffset;
boundingBox.Offset(voffset);
}
/// <summary>
/// Scales the line from the zero point.
/// </summary>
/// <param name="factor"></param>
public override void Scale(double factor)
{
pt1 *= factor;
pt2 *= factor;
}
/// <summary>
/// Scales the line from the origin.
/// </summary>
/// <param name="factor"></param>
/// <param name="origin"></param>
public override void Scale(double factor, Vector origin)
{
pt1 = (pt1 - origin) * factor + origin;
pt2 = (pt2 - origin) * factor + origin;
}
/// <summary>
/// Rotates the line from the zero point.
/// </summary>
/// <param name="angle"></param>
public override void Rotate(double angle)
{
StartPoint = StartPoint.Rotate(angle);
EndPoint = EndPoint.Rotate(angle);
}
/// <summary>
/// Rotates the line from the origin.
/// </summary>
/// <param name="angle"></param>
/// <param name="origin"></param>
public override void Rotate(double angle, Vector origin)
{
StartPoint = StartPoint.Rotate(angle, origin);
EndPoint = EndPoint.Rotate(angle, origin);
}
/// <summary>
/// Updates the bounding box.
/// </summary>
public override sealed void UpdateBounds()
{
if (StartPoint.X < EndPoint.X)
{
boundingBox.X = StartPoint.X;
boundingBox.Length = EndPoint.X - StartPoint.X;
}
else
{
boundingBox.X = EndPoint.X;
boundingBox.Length = StartPoint.X - EndPoint.X;
}
if (StartPoint.Y < EndPoint.Y)
{
boundingBox.Y = StartPoint.Y;
boundingBox.Width = EndPoint.Y - StartPoint.Y;
}
else
{
boundingBox.Y = EndPoint.Y;
boundingBox.Width = StartPoint.Y - EndPoint.Y;
}
}
public override Entity OffsetEntity(double distance, OffsetSide side)
{
var angle = OpenNest.Math.Angle.NormalizeRad(Angle() + OpenNest.Math.Angle.HalfPI);
var x = System.Math.Cos(angle) * distance;
var y = System.Math.Sin(angle) * distance;
var pt = new Vector(x, y);
return side == OffsetSide.Left
? new Line(StartPoint + pt, EndPoint + pt)
: new Line(EndPoint + pt, StartPoint + pt);
}
public override Entity OffsetEntity(double distance, Vector pt)
{
var a = pt - StartPoint;
var b = EndPoint - StartPoint;
var c = a.DotProduct(b);
var side = c < 0 ? OffsetSide.Left : OffsetSide.Right;
return OffsetEntity(distance, side);
}
/// <summary>
/// Splits the line at the given point, returning two sub-lines.
/// Either half may be null if the split point coincides with an endpoint.
/// </summary>
/// <param name="point">The point at which to split the line.</param>
/// <returns>A tuple of (first, second) sub-lines.</returns>
public (Line first, Line second) SplitAt(Vector point)
{
var first = point.DistanceTo(StartPoint) < Tolerance.Epsilon
? null
: new Line(StartPoint, point);
var second = point.DistanceTo(EndPoint) < Tolerance.Epsilon
? null
: new Line(point, EndPoint);
return (first, second);
}
/// <summary>
/// Gets the closest point on the line to the given point.
/// </summary>
/// <param name="pt"></param>
/// <returns></returns>
public override Vector ClosestPointTo(Vector pt)
{
var perpendicularPt = PointPerpendicularFrom(pt);
if (BoundingBox.Contains(perpendicularPt))
return perpendicularPt;
else
return pt.DistanceTo(StartPoint) <= pt.DistanceTo(EndPoint) ? StartPoint : EndPoint;
}
/// <summary>
/// Returns true if the given arc is intersecting this.
/// </summary>
/// <param name="arc"></param>
/// <returns></returns>
public override bool Intersects(Arc arc)
{
List<Vector> pts;
return Intersect.Intersects(arc, this, out pts);
}
/// <summary>
/// Returns true if the given arc is intersecting this.
/// </summary>
/// <param name="arc"></param>
/// <param name="pts"></param>
/// <returns></returns>
public override bool Intersects(Arc arc, out List<Vector> pts)
{
return Intersect.Intersects(arc, this, out pts);
}
/// <summary>
/// Returns true if the given circle is intersecting this.
/// </summary>
/// <param name="circle"></param>
/// <returns></returns>
public override bool Intersects(Circle circle)
{
List<Vector> pts;
return Intersect.Intersects(circle, this, out pts);
}
/// <summary>
/// Returns true if the given circle is intersecting this.
/// </summary>
/// <param name="circle"></param>
/// <param name="pts"></param>
/// <returns></returns>
public override bool Intersects(Circle circle, out List<Vector> pts)
{
return Intersect.Intersects(circle, this, out pts);
}
/// <summary>
/// Returns true if the given line is intersecting this.
/// </summary>
/// <param name="line"></param>
/// <returns></returns>
public override bool Intersects(Line line)
{
Vector pt;
return Intersects(line, out pt);
}
/// <summary>
/// Returns true if the given line is intersecting this.
/// </summary>
/// <param name="line"></param>
/// <param name="pts"></param>
/// <returns></returns>
public override bool Intersects(Line line, out List<Vector> pts)
{
Vector pt;
var success = Intersect.Intersects(this, line, out pt);
pts = new List<Vector>(new[] { pt });
return success;
}
/// <summary>
/// Returns true if the given polygon is intersecting this.
/// </summary>
/// <param name="polygon"></param>
/// <returns></returns>
public override bool Intersects(Polygon polygon)
{
List<Vector> pts;
return Intersect.Intersects(this, polygon, out pts);
}
/// <summary>
/// Returns true if the given polygon is intersecting this.
/// </summary>
/// <param name="polygon"></param>
/// <param name="pts"></param>
/// <returns></returns>
public override bool Intersects(Polygon polygon, out List<Vector> pts)
{
return Intersect.Intersects(this, polygon, out pts);
}
/// <summary>
/// Returns true if the given shape is intersecting this.
/// </summary>
/// <param name="shape"></param>
/// <returns></returns>
public override bool Intersects(Shape shape)
{
List<Vector> pts;
return Intersect.Intersects(this, shape, out pts);
}
/// <summary>
/// Returns true if the given shape is intersecting this.
/// </summary>
/// <param name="shape"></param>
/// <param name="pts"></param>
/// <returns></returns>
public override bool Intersects(Shape shape, out List<Vector> pts)
{
return Intersect.Intersects(this, shape, out pts);
}
/// <summary>
/// Type of entity.
/// </summary>
public override EntityType Type
{
get { return EntityType.Line; }
}
}
}