znlgis 博客

GIS开发与技术分享

第六章:二维图元系统

6.1 二维图元概述

6.1.1 图元分类

LightCAD的二维图元系统位于LightCAD.Core/Elements/Basic/目录下,包含了CAD系统中所有基本的二维几何实体。这些图元是构成复杂设计图纸的基本元素:

图元类型 类名 说明
直线 LcLine 由起点和终点定义的线段
圆弧 LcArc 由圆心、半径、起始角和终止角定义
LcCircle 由圆心和半径定义的完整圆
椭圆 LcEllipse 由中心、长轴、短轴和旋转角定义
多段线 LcPolyline 由顶点序列定义的折线或曲线
多边形 LcPolygon 封闭的多段线
样条曲线 LcSpline 通过控制点定义的光滑曲线

6.1.2 公共基类

所有二维图元都继承自LcEntity基类,共享以下属性:

// 所有2D图元的公共属性(来自LcEntity)
public string LayerName { get; set; }     // 图层
public LcColor Color { get; set; }        // 颜色
public string LineType { get; set; }      // 线型
public double LineWeight { get; set; }    // 线宽
public bool IsVisible { get; set; }       // 可见性

6.2 直线(LcLine)

6.2.1 数据结构

public class LcLine : LcEntity, IUpdateObject
{
    /// <summary>
    /// 直线起点
    /// </summary>
    public Point2d StartPoint { get; set; }

    /// <summary>
    /// 直线终点
    /// </summary>
    public Point2d EndPoint { get; set; }

    /// <summary>
    /// 直线长度
    /// </summary>
    public double Length => StartPoint.DistanceTo(EndPoint);

    /// <summary>
    /// 直线方向向量
    /// </summary>
    public Vector2d Direction => (EndPoint - StartPoint).Normalize();

    /// <summary>
    /// 直线中点
    /// </summary>
    public Point2d MidPoint => StartPoint.MidPoint(EndPoint);

    /// <summary>
    /// 直线与X轴的夹角
    /// </summary>
    public double Angle
    {
        get
        {
            var dir = EndPoint - StartPoint;
            return Math.Atan2(dir.Y, dir.X);
        }
    }

    public override string TypeName => "Line";

    public override BoundingBox2d Bounds
    {
        get
        {
            return new BoundingBox2d
            {
                Min = new Point2d(
                    Math.Min(StartPoint.X, EndPoint.X),
                    Math.Min(StartPoint.Y, EndPoint.Y)),
                Max = new Point2d(
                    Math.Max(StartPoint.X, EndPoint.X),
                    Math.Max(StartPoint.Y, EndPoint.Y))
            };
        }
    }
}

6.2.2 构造方法

// 方法1:通过两个点创建
var line1 = new LcLine
{
    StartPoint = new Point2d(0, 0),
    EndPoint = new Point2d(100, 50)
};

// 方法2:通过起点、角度和长度创建
public static LcLine FromAngleAndLength(
    Point2d start, double angle, double length)
{
    var end = new Point2d(
        start.X + length * Math.Cos(angle),
        start.Y + length * Math.Sin(angle)
    );
    return new LcLine { StartPoint = start, EndPoint = end };
}

// 方法3:通过起点和偏移量创建
public static LcLine FromOffset(
    Point2d start, double dx, double dy)
{
    return new LcLine
    {
        StartPoint = start,
        EndPoint = new Point2d(start.X + dx, start.Y + dy)
    };
}

6.2.3 几何操作

public class LcLine : LcEntity
{
    /// <summary>
    /// 计算点到直线的最近点
    /// </summary>
    public Point2d ClosestPoint(Point2d point)
    {
        return GeometryUtils.ClosestPointOnSegment(
            point, StartPoint, EndPoint);
    }

    /// <summary>
    /// 计算点到直线的距离
    /// </summary>
    public override double DistanceTo(Point2d point)
    {
        var closest = ClosestPoint(point);
        return point.DistanceTo(closest);
    }

    /// <summary>
    /// 在指定参数位置分割直线
    /// </summary>
    public (LcLine first, LcLine second) SplitAt(double t)
    {
        var splitPoint = new Point2d(
            StartPoint.X + t * (EndPoint.X - StartPoint.X),
            StartPoint.Y + t * (EndPoint.Y - StartPoint.Y)
        );

        return (
            new LcLine { StartPoint = StartPoint, EndPoint = splitPoint },
            new LcLine { StartPoint = splitPoint, EndPoint = EndPoint }
        );
    }

    /// <summary>
    /// 延伸直线到指定长度
    /// </summary>
    public void ExtendTo(double newLength)
    {
        var dir = Direction;
        EndPoint = StartPoint + dir * newLength;
    }

    /// <summary>
    /// 修剪直线到指定的边界
    /// </summary>
    public void TrimTo(Point2d boundaryStart, Vector2d boundaryDir)
    {
        var intersection = LineLineIntersect.Compute(
            StartPoint, Direction, boundaryStart, boundaryDir);

        if (intersection.HasValue)
        {
            EndPoint = intersection.Value;
        }
    }

    /// <summary>
    /// 几何变换
    /// </summary>
    public override void TransformBy(Matrix4d matrix)
    {
        var start3d = new Point3d(StartPoint.X, StartPoint.Y, 0);
        var end3d = new Point3d(EndPoint.X, EndPoint.Y, 0);

        var newStart = matrix.Transform(start3d);
        var newEnd = matrix.Transform(end3d);

        StartPoint = new Point2d(newStart.X, newStart.Y);
        EndPoint = new Point2d(newEnd.X, newEnd.Y);
    }

    /// <summary>
    /// 克隆
    /// </summary>
    public override LcEntity Clone()
    {
        return new LcLine
        {
            StartPoint = StartPoint,
            EndPoint = EndPoint,
            Color = Color,
            LayerName = LayerName,
            LineType = LineType,
            LineWeight = LineWeight
        };
    }
}

6.3 圆弧(LcArc)

6.3.1 数据结构

public class LcArc : LcEntity, IUpdateObject
{
    /// <summary>
    /// 圆心
    /// </summary>
    public Point2d Center { get; set; }

    /// <summary>
    /// 半径
    /// </summary>
    public double Radius { get; set; }

    /// <summary>
    /// 起始角度(弧度)
    /// </summary>
    public double StartAngle { get; set; }

    /// <summary>
    /// 终止角度(弧度)
    /// </summary>
    public double EndAngle { get; set; }

    /// <summary>
    /// 是否为逆时针方向
    /// </summary>
    public bool IsCounterClockwise { get; set; } = true;

    /// <summary>
    /// 圆弧长度
    /// </summary>
    public double Length
    {
        get
        {
            var sweep = SweepAngle;
            return Radius * Math.Abs(sweep);
        }
    }

    /// <summary>
    /// 扫掠角度
    /// </summary>
    public double SweepAngle
    {
        get
        {
            var sweep = EndAngle - StartAngle;
            if (IsCounterClockwise && sweep < 0)
                sweep += 2 * Math.PI;
            if (!IsCounterClockwise && sweep > 0)
                sweep -= 2 * Math.PI;
            return sweep;
        }
    }

    /// <summary>
    /// 起点
    /// </summary>
    public Point2d StartPoint => ArcMath.PointOnArc(Center, Radius, StartAngle);

    /// <summary>
    /// 终点
    /// </summary>
    public Point2d EndPoint => ArcMath.PointOnArc(Center, Radius, EndAngle);

    /// <summary>
    /// 中点
    /// </summary>
    public Point2d MidPoint
    {
        get
        {
            var midAngle = StartAngle + SweepAngle / 2;
            return ArcMath.PointOnArc(Center, Radius, midAngle);
        }
    }

    public override string TypeName => "Arc";
}

6.3.2 圆弧的创建方式

// 方法1:圆心、半径、起止角度
var arc1 = new LcArc
{
    Center = new Point2d(50, 50),
    Radius = 30,
    StartAngle = 0,
    EndAngle = Math.PI / 2  // 90度圆弧
};

// 方法2:三点定圆弧
public static LcArc FromThreePoints(Point2d start, Point2d through, Point2d end)
{
    var arcData = ArcMath.FromThreePoints(start, through, end);
    return new LcArc
    {
        Center = arcData.Center,
        Radius = arcData.Radius,
        StartAngle = arcData.StartAngle,
        EndAngle = arcData.EndAngle
    };
}

// 方法3:起点、终点、凸度
public static LcArc FromBulge(Point2d start, Point2d end, double bulge)
{
    // bulge = tan(sweepAngle/4)
    var chord = start.DistanceTo(end);
    var sagitta = Math.Abs(bulge) * chord / 2;
    var radius = (chord * chord / 4 + sagitta * sagitta) / (2 * sagitta);

    var midPoint = start.MidPoint(end);
    var chordDir = (end - start).Normalize();
    var normalDir = chordDir.Perpendicular();

    var offset = radius - sagitta;
    var center = midPoint + normalDir * (bulge > 0 ? -offset : offset);

    return new LcArc
    {
        Center = center,
        Radius = radius,
        StartAngle = Math.Atan2(start.Y - center.Y, start.X - center.X),
        EndAngle = Math.Atan2(end.Y - center.Y, end.X - center.X),
        IsCounterClockwise = bulge > 0
    };
}

6.3.3 离散化

/// <summary>
/// 将圆弧离散为折线段(用于渲染)
/// </summary>
public List<Point2d> Tessellate(int segments = 0)
{
    // 如果未指定段数,根据半径和扫掠角自动计算
    if (segments <= 0)
    {
        segments = Math.Max(8, (int)(Math.Abs(SweepAngle) * Radius / 5));
        segments = Math.Min(segments, 360);
    }

    return ArcMath.Tessellate(Center, Radius, StartAngle, EndAngle, segments);
}

6.4 圆(LcCircle)

6.4.1 数据结构

public class LcCircle : LcEntity, IUpdateObject
{
    /// <summary>
    /// 圆心
    /// </summary>
    public Point2d Center { get; set; }

    /// <summary>
    /// 半径
    /// </summary>
    public double Radius { get; set; }

    /// <summary>
    /// 周长
    /// </summary>
    public double Circumference => 2 * Math.PI * Radius;

    /// <summary>
    /// 面积
    /// </summary>
    public double Area => Math.PI * Radius * Radius;

    /// <summary>
    /// 直径
    /// </summary>
    public double Diameter => 2 * Radius;

    public override string TypeName => "Circle";

    public override BoundingBox2d Bounds
    {
        get
        {
            return new BoundingBox2d
            {
                Min = new Point2d(Center.X - Radius, Center.Y - Radius),
                Max = new Point2d(Center.X + Radius, Center.Y + Radius)
            };
        }
    }

    /// <summary>
    /// 判断点是否在圆内
    /// </summary>
    public bool ContainsPoint(Point2d point)
    {
        return Center.DistanceTo(point) <= Radius + Tolerance.Distance;
    }

    /// <summary>
    /// 获取圆上指定角度的点
    /// </summary>
    public Point2d PointAt(double angle)
    {
        return new Point2d(
            Center.X + Radius * Math.Cos(angle),
            Center.Y + Radius * Math.Sin(angle)
        );
    }

    /// <summary>
    /// 获取从外部点到圆的切线
    /// </summary>
    public (LcLine tangent1, LcLine tangent2)? GetTangentLines(Point2d point)
    {
        var dist = Center.DistanceTo(point);
        if (dist <= Radius) return null; // 点在圆内

        var angle = Math.Acos(Radius / dist);
        var dirAngle = Math.Atan2(
            Center.Y - point.Y, Center.X - point.X);

        var tangentPoint1 = PointAt(dirAngle + Math.PI + angle);
        var tangentPoint2 = PointAt(dirAngle + Math.PI - angle);

        return (
            new LcLine { StartPoint = point, EndPoint = tangentPoint1 },
            new LcLine { StartPoint = point, EndPoint = tangentPoint2 }
        );
    }
}

6.4.2 圆的创建方式

// 方法1:圆心和半径
var circle1 = new LcCircle
{
    Center = new Point2d(100, 100),
    Radius = 50
};

// 方法2:两点确定(对径点)
public static LcCircle FromDiameter(Point2d p1, Point2d p2)
{
    var center = p1.MidPoint(p2);
    var radius = p1.DistanceTo(p2) / 2;
    return new LcCircle { Center = center, Radius = radius };
}

// 方法3:三点确定
public static LcCircle FromThreePoints(Point2d p1, Point2d p2, Point2d p3)
{
    var arcData = ArcMath.FromThreePoints(p1, p2, p3);
    return new LcCircle
    {
        Center = arcData.Center,
        Radius = arcData.Radius
    };
}

// 方法4:相切于两条线(TTR - 相切、相切、半径)
public static LcCircle TangentTangentRadius(
    LcLine line1, LcLine line2, double radius)
{
    // 计算两条线的偏移线(距离为radius)
    // 然后求偏移线的交点作为圆心
    // ...
}

6.5 椭圆(LcEllipse)

6.5.1 数据结构

public class LcEllipse : LcEntity, IUpdateObject
{
    /// <summary>
    /// 椭圆中心
    /// </summary>
    public Point2d Center { get; set; }

    /// <summary>
    /// 长半轴长度
    /// </summary>
    public double MajorRadius { get; set; }

    /// <summary>
    /// 短半轴长度
    /// </summary>
    public double MinorRadius { get; set; }

    /// <summary>
    /// 旋转角度(长轴与X轴的夹角,弧度)
    /// </summary>
    public double Rotation { get; set; }

    /// <summary>
    /// 起始参数(用于椭圆弧)
    /// </summary>
    public double StartParameter { get; set; } = 0;

    /// <summary>
    /// 终止参数(用于椭圆弧,2π表示完整椭圆)
    /// </summary>
    public double EndParameter { get; set; } = 2 * Math.PI;

    /// <summary>
    /// 是否为完整椭圆
    /// </summary>
    public bool IsFullEllipse =>
        Math.Abs(EndParameter - StartParameter - 2 * Math.PI) < 1e-10;

    /// <summary>
    /// 离心率
    /// </summary>
    public double Eccentricity
    {
        get
        {
            var a = Math.Max(MajorRadius, MinorRadius);
            var b = Math.Min(MajorRadius, MinorRadius);
            return Math.Sqrt(1 - (b * b) / (a * a));
        }
    }

    public override string TypeName => "Ellipse";

    /// <summary>
    /// 获取椭圆上指定参数处的点
    /// </summary>
    public Point2d PointAt(double t)
    {
        var cos = Math.Cos(Rotation);
        var sin = Math.Sin(Rotation);
        var x = MajorRadius * Math.Cos(t);
        var y = MinorRadius * Math.Sin(t);

        return new Point2d(
            Center.X + x * cos - y * sin,
            Center.Y + x * sin + y * cos
        );
    }

    /// <summary>
    /// 周长(近似计算,使用Ramanujan公式)
    /// </summary>
    public double Circumference
    {
        get
        {
            var a = MajorRadius;
            var b = MinorRadius;
            var h = Math.Pow(a - b, 2) / Math.Pow(a + b, 2);
            return Math.PI * (a + b) * (1 + 3 * h / (10 + Math.Sqrt(4 - 3 * h)));
        }
    }
}

6.6 多段线(LcPolyline)

6.6.1 数据结构

多段线是CAD中最常用的图元之一,由一系列顶点组成,顶点之间可以是直线段或圆弧段:

public class LcPolyline : LcEntity, IUpdateObject
{
    /// <summary>
    /// 顶点集合
    /// </summary>
    public List<PolylineVertex> Vertices { get; set; } = new();

    /// <summary>
    /// 是否闭合
    /// </summary>
    public bool IsClosed { get; set; }

    /// <summary>
    /// 全局宽度
    /// </summary>
    public double GlobalWidth { get; set; }

    /// <summary>
    /// 顶点数量
    /// </summary>
    public int VertexCount => Vertices.Count;

    /// <summary>
    /// 线段数量
    /// </summary>
    public int SegmentCount => IsClosed ? Vertices.Count : Vertices.Count - 1;

    public override string TypeName => "Polyline";

    /// <summary>
    /// 总长度
    /// </summary>
    public double Length
    {
        get
        {
            double length = 0;
            for (int i = 0; i < SegmentCount; i++)
            {
                length += GetSegmentLength(i);
            }
            return length;
        }
    }

    /// <summary>
    /// 面积(仅对闭合多段线有效)
    /// </summary>
    public double Area
    {
        get
        {
            if (!IsClosed) return 0;
            var points = Vertices.Select(v => v.Position).ToList();
            return GeometryUtils.PolygonArea(points);
        }
    }
}

/// <summary>
/// 多段线顶点
/// </summary>
public struct PolylineVertex
{
    /// <summary>
    /// 顶点坐标
    /// </summary>
    public Point2d Position { get; set; }

    /// <summary>
    /// 凸度(0为直线段,非0为圆弧段)
    /// bulge = tan(包含角/4)
    /// 正值为逆时针,负值为顺时针
    /// </summary>
    public double Bulge { get; set; }

    /// <summary>
    /// 起始宽度
    /// </summary>
    public double StartWidth { get; set; }

    /// <summary>
    /// 终止宽度
    /// </summary>
    public double EndWidth { get; set; }

    public PolylineVertex(Point2d position, double bulge = 0)
    {
        Position = position;
        Bulge = bulge;
        StartWidth = 0;
        EndWidth = 0;
    }

    /// <summary>
    /// 是否为圆弧段
    /// </summary>
    public bool IsArc => Math.Abs(Bulge) > 1e-10;
}

6.6.2 多段线操作

public class LcPolyline : LcEntity
{
    /// <summary>
    /// 添加顶点
    /// </summary>
    public void AddVertex(Point2d point, double bulge = 0)
    {
        Vertices.Add(new PolylineVertex(point, bulge));
    }

    /// <summary>
    /// 在指定位置插入顶点
    /// </summary>
    public void InsertVertex(int index, Point2d point, double bulge = 0)
    {
        Vertices.Insert(index, new PolylineVertex(point, bulge));
    }

    /// <summary>
    /// 删除顶点
    /// </summary>
    public void RemoveVertex(int index)
    {
        Vertices.RemoveAt(index);
    }

    /// <summary>
    /// 获取指定段的起止顶点
    /// </summary>
    public (PolylineVertex start, PolylineVertex end) GetSegment(int index)
    {
        var start = Vertices[index];
        var end = Vertices[(index + 1) % Vertices.Count];
        return (start, end);
    }

    /// <summary>
    /// 获取指定段的长度
    /// </summary>
    public double GetSegmentLength(int index)
    {
        var (start, end) = GetSegment(index);
        if (start.IsArc)
        {
            // 圆弧段长度
            var arc = LcArc.FromBulge(start.Position, end.Position, start.Bulge);
            return arc.Length;
        }
        else
        {
            // 直线段长度
            return start.Position.DistanceTo(end.Position);
        }
    }

    /// <summary>
    /// 反转多段线方向
    /// </summary>
    public void Reverse()
    {
        Vertices.Reverse();
        // 反转凸度符号
        for (int i = 0; i < Vertices.Count; i++)
        {
            var v = Vertices[i];
            Vertices[i] = new PolylineVertex(v.Position, -v.Bulge);
        }
    }

    /// <summary>
    /// 偏移多段线
    /// </summary>
    public LcPolyline Offset(double distance)
    {
        var result = new LcPolyline { IsClosed = IsClosed };

        for (int i = 0; i < SegmentCount; i++)
        {
            var (start, end) = GetSegment(i);
            var dir = (end.Position - start.Position).Normalize();
            var normal = dir.Perpendicular();

            result.AddVertex(
                start.Position + normal * distance,
                start.Bulge);
        }

        if (!IsClosed)
        {
            var lastVertex = Vertices.Last();
            var prevVertex = Vertices[Vertices.Count - 2];
            var dir = (lastVertex.Position - prevVertex.Position).Normalize();
            var normal = dir.Perpendicular();
            result.AddVertex(lastVertex.Position + normal * distance);
        }

        return result;
    }

    /// <summary>
    /// 离散化为点列表
    /// </summary>
    public List<Point2d> Tessellate(int arcSegments = 16)
    {
        var points = new List<Point2d>();

        for (int i = 0; i < SegmentCount; i++)
        {
            var (start, end) = GetSegment(i);
            points.Add(start.Position);

            if (start.IsArc)
            {
                // 将圆弧段离散为折线点
                var arc = LcArc.FromBulge(
                    start.Position, end.Position, start.Bulge);
                var arcPoints = arc.Tessellate(arcSegments);
                // 跳过第一个和最后一个点(避免重复)
                for (int j = 1; j < arcPoints.Count - 1; j++)
                {
                    points.Add(arcPoints[j]);
                }
            }
        }

        // 添加最后一个顶点(非闭合情况)
        if (!IsClosed && Vertices.Count > 0)
        {
            points.Add(Vertices.Last().Position);
        }

        return points;
    }
}

6.7 多边形(LcPolygon)

6.7.1 数据结构

LcPolygon本质上是一个闭合的多段线,但提供了额外的面域相关操作:

public class LcPolygon : LcEntity, IUpdateObject
{
    /// <summary>
    /// 顶点集合
    /// </summary>
    public List<Point2d> Vertices { get; set; } = new();

    /// <summary>
    /// 面积
    /// </summary>
    public double Area => GeometryUtils.PolygonArea(Vertices);

    /// <summary>
    /// 周长
    /// </summary>
    public double Perimeter
    {
        get
        {
            double perimeter = 0;
            for (int i = 0; i < Vertices.Count; i++)
            {
                int j = (i + 1) % Vertices.Count;
                perimeter += Vertices[i].DistanceTo(Vertices[j]);
            }
            return perimeter;
        }
    }

    /// <summary>
    /// 重心
    /// </summary>
    public Point2d Centroid
    {
        get
        {
            double cx = 0, cy = 0;
            double signedArea = 0;

            for (int i = 0; i < Vertices.Count; i++)
            {
                int j = (i + 1) % Vertices.Count;
                var cross = Vertices[i].X * Vertices[j].Y -
                           Vertices[j].X * Vertices[i].Y;
                signedArea += cross;
                cx += (Vertices[i].X + Vertices[j].X) * cross;
                cy += (Vertices[i].Y + Vertices[j].Y) * cross;
            }

            signedArea /= 2;
            cx /= (6 * signedArea);
            cy /= (6 * signedArea);

            return new Point2d(cx, cy);
        }
    }

    /// <summary>
    /// 判断点是否在多边形内
    /// </summary>
    public bool ContainsPoint(Point2d point)
    {
        return GeometryUtils.PointInPolygon(point, Vertices);
    }

    /// <summary>
    /// 是否为凸多边形
    /// </summary>
    public bool IsConvex
    {
        get
        {
            int n = Vertices.Count;
            if (n < 3) return false;

            bool? sign = null;
            for (int i = 0; i < n; i++)
            {
                var d1 = Vertices[(i + 1) % n] - Vertices[i];
                var d2 = Vertices[(i + 2) % n] - Vertices[(i + 1) % n];
                var cross = d1.Cross(d2);

                if (Math.Abs(cross) > 1e-10)
                {
                    bool positive = cross > 0;
                    if (sign.HasValue && sign.Value != positive)
                        return false;
                    sign = positive;
                }
            }

            return true;
        }
    }

    public override string TypeName => "Polygon";
}

6.7.2 正多边形创建

/// <summary>
/// 创建正多边形
/// </summary>
public static LcPolygon CreateRegular(
    Point2d center, double radius, int sides, double startAngle = 0)
{
    var polygon = new LcPolygon();

    for (int i = 0; i < sides; i++)
    {
        var angle = startAngle + 2 * Math.PI * i / sides;
        polygon.Vertices.Add(new Point2d(
            center.X + radius * Math.Cos(angle),
            center.Y + radius * Math.Sin(angle)
        ));
    }

    return polygon;
}

// 使用示例
var hexagon = LcPolygon.CreateRegular(
    new Point2d(50, 50), 30, 6);  // 六边形
var pentagon = LcPolygon.CreateRegular(
    new Point2d(150, 50), 30, 5); // 五边形

6.8 样条曲线(LcSpline)

6.8.1 数据结构

public class LcSpline : LcEntity, IUpdateObject
{
    /// <summary>
    /// 拟合点(曲线经过的点)
    /// </summary>
    public List<Point2d> FitPoints { get; set; } = new();

    /// <summary>
    /// 控制点(用于NURBS表示)
    /// </summary>
    public List<Point2d> ControlPoints { get; set; } = new();

    /// <summary>
    /// 节点向量
    /// </summary>
    public List<double> KnotVector { get; set; } = new();

    /// <summary>
    /// 曲线阶数
    /// </summary>
    public int Degree { get; set; } = 3;

    /// <summary>
    /// 权重
    /// </summary>
    public List<double> Weights { get; set; } = new();

    /// <summary>
    /// 是否闭合
    /// </summary>
    public bool IsClosed { get; set; }

    /// <summary>
    /// 起点切线方向
    /// </summary>
    public Vector2d? StartTangent { get; set; }

    /// <summary>
    /// 终点切线方向
    /// </summary>
    public Vector2d? EndTangent { get; set; }

    public override string TypeName => "Spline";

    /// <summary>
    /// 计算曲线上指定参数处的点
    /// </summary>
    public Point2d PointAt(double t)
    {
        if (ControlPoints.Count > 0 && KnotVector.Count > 0)
        {
            // NURBS求值
            var nurbs = new NurbsCurve(
                ControlPoints.Select(
                    p => new Point3d(p.X, p.Y, 0)).ToArray(),
                Weights.Count > 0 ?
                    Weights.ToArray() :
                    Enumerable.Repeat(1.0, ControlPoints.Count).ToArray(),
                KnotVector.ToArray(),
                Degree
            );
            var result = nurbs.Evaluate(t);
            return new Point2d(result.X, result.Y);
        }
        else if (FitPoints.Count >= 2)
        {
            // 通过拟合点插值
            return InterpolateFitPoints(t);
        }

        return Point2d.Origin;
    }

    /// <summary>
    /// 离散化为点列表
    /// </summary>
    public List<Point2d> Tessellate(int segments = 64)
    {
        var points = new List<Point2d>(segments + 1);
        for (int i = 0; i <= segments; i++)
        {
            var t = (double)i / segments;
            points.Add(PointAt(t));
        }
        return points;
    }
}

6.9 图元的通用操作

6.9.1 几何变换

所有二维图元都支持以下几何变换:

// 平移
var translation = Matrix4d.CreateTranslation(dx, dy, 0);
entity.TransformBy(translation);

// 旋转(绕指定点)
var toOrigin = Matrix4d.CreateTranslation(-centerX, -centerY, 0);
var rotation = Matrix4d.CreateRotationZ(angle);
var backToPos = Matrix4d.CreateTranslation(centerX, centerY, 0);
entity.TransformBy(backToPos * rotation * toOrigin);

// 缩放(绕指定点)
var scale = Matrix4d.CreateScale(sx, sy, 1);
entity.TransformBy(backToPos * scale * toOrigin);

// 镜像(沿指定轴线)
public static Matrix4d CreateMirror(Point2d linePoint, Vector2d lineDir)
{
    var dir = lineDir.Normalize();
    // 反射矩阵公式
    return new Matrix4d(
        2 * dir.X * dir.X - 1, 2 * dir.X * dir.Y, 0, 0,
        2 * dir.X * dir.Y, 2 * dir.Y * dir.Y - 1, 0, 0,
        0, 0, 1, 0,
        0, 0, 0, 1
    );
}

6.9.2 图元拾取(命中测试)

/// <summary>
/// 判断点击位置是否命中图元
/// </summary>
public static bool HitTest(LcEntity entity, Point2d clickPoint, double tolerance)
{
    return entity.DistanceTo(clickPoint) <= tolerance;
}

/// <summary>
/// 窗口选择(全包含)
/// </summary>
public static IEnumerable<LcEntity> WindowSelect(
    IEnumerable<LcEntity> entities, BoundingBox2d window)
{
    return entities.Where(e =>
        window.Contains(e.Bounds.Min) &&
        window.Contains(e.Bounds.Max));
}

/// <summary>
/// 交叉选择(相交即选中)
/// </summary>
public static IEnumerable<LcEntity> CrossingSelect(
    IEnumerable<LcEntity> entities, BoundingBox2d window)
{
    return entities.Where(e => e.Bounds.Intersects(window));
}

6.10 本章小结

本章详细介绍了LightCAD的二维图元系统,包括直线、圆弧、圆、椭圆、多段线、多边形和样条曲线七种基本图元。每种图元都有明确的数据结构定义、多种创建方式和丰富的几何操作方法。多段线通过凸度(Bulge)机制实现了直线段和圆弧段的混合表示,是CAD中最灵活的图元类型。所有图元通过统一的变换接口支持平移、旋转、缩放和镜像操作。


上一章第五章:核心数据模型详解

下一章第七章:三维图元系统