二维绘图与交互操作

第八章:二维绘图与交互操作

8.1 二维绘图系统概述

8.1.1 绘图架构

LightCAD的二维绘图系统基于SkiaSharp图形引擎,提供高性能的2D渲染能力:

┌─────────────────────────────────────────┐
│           ElementAction.Draw()           │
│            (元素绘制入口)                 │
├─────────────────────────────────────────┤
│              LcCanvas2d                  │
│           (二维画布抽象层)                │
├─────────────────────────────────────────┤
│              SkiaSharp                   │
│           (底层图形引擎)                  │
├─────────────────────────────────────────┤
│           GPU / Software                 │
│            (硬件/软件渲染)                │
└─────────────────────────────────────────┘

8.1.2 核心类

类名 功能
LcCanvas2d 二维画布,提供各种绘图方法
LcPaint 画笔,定义线条样式、颜色、宽度等
LcTextPaint 文字画笔,定义字体、大小、颜色等
Matrix3 3×3变换矩阵,用于坐标转换

8.2 LcCanvas2d画布

8.2.1 基本绘图方法

public class LcCanvas2d
{
    /// <summary>
    /// 绘制直线
    /// </summary>
    public void DrawLine(LcPaint paint, Vector2 start, Vector2 end);
    
    /// <summary>
    /// 绘制曲线
    /// </summary>
    public void DrawCurve(LcPaint paint, Curve2d curve, Matrix3 matrix);
    
    /// <summary>
    /// 绘制多条曲线
    /// </summary>
    public void DrawCurves(LcPaint paint, Curve2d[] curves, Matrix3 matrix);
    
    /// <summary>
    /// 绘制圆
    /// </summary>
    public void DrawCircle(LcPaint paint, Vector2 center, double radius);
    
    /// <summary>
    /// 绘制圆弧
    /// </summary>
    public void DrawArc(LcPaint paint, Vector2 center, double radius, 
                        double startAngle, double endAngle);
    
    /// <summary>
    /// 绘制矩形
    /// </summary>
    public void DrawRect(LcPaint paint, Box2 rect);
    
    /// <summary>
    /// 绘制填充矩形
    /// </summary>
    public void FillRect(LcPaint paint, Box2 rect);
    
    /// <summary>
    /// 绘制多边形
    /// </summary>
    public void DrawPolygon(LcPaint paint, Vector2[] points);
    
    /// <summary>
    /// 绘制填充多边形
    /// </summary>
    public void FillPolygon(LcPaint paint, Vector2[] points);
    
    /// <summary>
    /// 绘制文字
    /// </summary>
    public void DrawText(LcTextPaint paint, string text, Matrix3 matrix, 
                         out List<Box2> charBoxes);
    
    /// <summary>
    /// 绘制图像
    /// </summary>
    public void DrawImage(Image image, Box2 destRect);
}

8.2.2 绘图示例

public override void Draw(LcCanvas2d canvas, LcElement element, Matrix3 matrix)
{
    var myElement = element as MyElement;
    
    // 创建画笔
    var pen = new LcPaint
    {
        Color = new Color(0xFF0000),  // 红色
        Width = 2,
        StrokeStyle = StrokeStyle.Solid
    };
    
    // 绘制轮廓
    foreach (var curve in myElement.GetShapes()[0].Curve2ds)
    {
        canvas.DrawCurve(pen, curve, matrix);
    }
    
    // 绘制中心点
    var centerPen = new LcPaint
    {
        Color = new Color(0x0000FF),
        Width = 1
    };
    var center = myElement.BoundingBox.Center.ApplyMatrix3(matrix);
    canvas.DrawCircle(centerPen, center, 50);
    
    // 绘制文字标注
    var textPaint = new LcTextPaint
    {
        Color = new Color(0x000000),
        FontName = "仿宋",
        Size = 500,
        Position = center
    };
    canvas.DrawText(textPaint, "标注文字", matrix, out _);
}

8.3 LcPaint画笔

8.3.1 画笔属性

public class LcPaint
{
    /// <summary>
    /// 颜色
    /// </summary>
    public Color Color { get; set; }
    
    /// <summary>
    /// 线宽
    /// </summary>
    public double Width { get; set; }
    
    /// <summary>
    /// 线型样式
    /// </summary>
    public StrokeStyle StrokeStyle { get; set; }
    
    /// <summary>
    /// 虚线模式(当StrokeStyle为Custom时使用)
    /// </summary>
    public float[] DashPattern { get; set; }
    
    /// <summary>
    /// 端点样式
    /// </summary>
    public StrokeCap StrokeCap { get; set; }
    
    /// <summary>
    /// 连接样式
    /// </summary>
    public StrokeJoin StrokeJoin { get; set; }
    
    /// <summary>
    /// 透明度(0-255)
    /// </summary>
    public byte Alpha { get; set; }
    
    /// <summary>
    /// 是否填充
    /// </summary>
    public bool IsFill { get; set; }
}

public enum StrokeStyle
{
    Solid,      // 实线
    Dashed,     // 虚线
    Dotted,     // 点线
    DashDot,    // 点划线
    Custom      // 自定义
}

public enum StrokeCap
{
    Butt,       // 平头
    Round,      // 圆头
    Square      // 方头
}

public enum StrokeJoin
{
    Miter,      // 尖角
    Round,      // 圆角
    Bevel       // 斜角
}

8.3.2 画笔使用示例

// 实线画笔
var solidPen = new LcPaint
{
    Color = new Color(0x000000),
    Width = 1,
    StrokeStyle = StrokeStyle.Solid
};

// 虚线画笔
var dashedPen = new LcPaint
{
    Color = new Color(0xFF0000),
    Width = 2,
    StrokeStyle = StrokeStyle.Dashed
};

// 自定义虚线画笔
var customPen = new LcPaint
{
    Color = new Color(0x0000FF),
    Width = 1,
    StrokeStyle = StrokeStyle.Custom,
    DashPattern = new float[] { 10, 5, 2, 5 }  // 长虚线-短间隔-点-短间隔
};

// 填充画笔
var fillPen = new LcPaint
{
    Color = new Color(0x00FF00),
    IsFill = true,
    Alpha = 128  // 半透明
};

8.4 文字绘制

8.4.1 LcTextPaint属性

public class LcTextPaint
{
    /// <summary>
    /// 文字颜色
    /// </summary>
    public Color Color { get; set; }
    
    /// <summary>
    /// 字体名称
    /// </summary>
    public string FontName { get; set; }
    
    /// <summary>
    /// 备用字体名称(用于中文)
    /// </summary>
    public string FontName2 { get; set; }
    
    /// <summary>
    /// 字体大小
    /// </summary>
    public double Size { get; set; }
    
    /// <summary>
    /// 宽度因子
    /// </summary>
    public double WidthFactor { get; set; }
    
    /// <summary>
    /// 字间距
    /// </summary>
    public double WordSpace { get; set; }
    
    /// <summary>
    /// 行间距
    /// </summary>
    public double LineSpace { get; set; }
    
    /// <summary>
    /// 位置
    /// </summary>
    public Vector2 Position { get; set; }
    
    /// <summary>
    /// 旋转角度
    /// </summary>
    public double Rotation { get; set; }
    
    /// <summary>
    /// 水平对齐
    /// </summary>
    public TextAlign HorizontalAlign { get; set; }
    
    /// <summary>
    /// 垂直对齐
    /// </summary>
    public TextVAlign VerticalAlign { get; set; }
}

public enum TextAlign
{
    Left,
    Center,
    Right
}

public enum TextVAlign
{
    Top,
    Middle,
    Bottom
}

8.4.2 文字绘制示例

public void DrawElementLabel(LcCanvas2d canvas, LcElement element, Matrix3 matrix)
{
    var textPaint = new LcTextPaint
    {
        Color = new Color(0x000000),
        FontName = "Arial",
        FontName2 = "仿宋",  // 中文备用字体
        Size = 800,
        WidthFactor = 1.0,
        WordSpace = 50,
        Position = element.BoundingBox.Center,
        HorizontalAlign = TextAlign.Center,
        VerticalAlign = TextVAlign.Middle
    };
    
    canvas.DrawText(textPaint, element.Type.DispalyName, matrix, out var charBoxes);
    
    // charBoxes 包含每个字符的包围盒,可用于文字拾取
}

8.5 坐标变换

8.5.1 Matrix3变换矩阵

public class Matrix3
{
    /// <summary>
    /// 创建平移矩阵
    /// </summary>
    public Matrix3 MakeTranslation(double x, double y);
    
    /// <summary>
    /// 创建旋转矩阵
    /// </summary>
    public Matrix3 MakeRotation(double angle);
    
    /// <summary>
    /// 创建缩放矩阵
    /// </summary>
    public Matrix3 MakeScale(double sx, double sy);
    
    /// <summary>
    /// 矩阵乘法
    /// </summary>
    public Matrix3 Multiply(Matrix3 other);
    
    /// <summary>
    /// 求逆矩阵
    /// </summary>
    public Matrix3 Invert();
    
    /// <summary>
    /// 单位矩阵
    /// </summary>
    public static Matrix3 Identity { get; }
}

// Vector2扩展
public static class Vector2Extensions
{
    /// <summary>
    /// 应用矩阵变换
    /// </summary>
    public static Vector2 ApplyMatrix3(this Vector2 v, Matrix3 matrix);
}

8.5.2 坐标变换使用

public override void Draw(LcCanvas2d canvas, LcElement element, Matrix3 matrix)
{
    // matrix 参数是当前视口的变换矩阵(世界坐标→屏幕坐标)
    
    // 获取元素的本地点
    var localPoint = new Vector2(100, 200);
    
    // 转换到屏幕坐标
    var screenPoint = localPoint.ApplyMatrix3(matrix);
    
    // 绘制时需要应用变换矩阵
    canvas.DrawCircle(pen, screenPoint, 10);
    
    // 或者直接使用支持矩阵参数的方法
    var curve = new Line2d(new Vector2(0, 0), new Vector2(100, 100));
    canvas.DrawCurve(pen, curve, matrix);  // 自动应用变换
}

8.6 交互操作

8.6.1 PointInputer点输入

public class PointInputer
{
    private IDocumentEditor docEditor;
    
    public PointInputer(IDocumentEditor docEditor)
    {
        this.docEditor = docEditor;
    }
    
    /// <summary>
    /// 执行点输入
    /// </summary>
    public async Task<PointInputResult> Execute(string prompt = null);
    
    /// <summary>
    /// 设置基点(用于相对坐标输入)
    /// </summary>
    public void SetBasePoint(Vector2 basePoint);
    
    /// <summary>
    /// 设置选项
    /// </summary>
    public void SetOptions(params string[] options);
}

public class PointInputResult
{
    public InputStatus Status { get; set; }
    public Vector2 Point { get; set; }
    public string Option { get; set; }  // 用户选择的选项
    public string Text { get; set; }    // 用户输入的文本
}

public enum InputStatus
{
    OK,         // 成功
    Cancel,     // 取消
    Option,     // 选择了选项
    Keyword     // 输入了关键字
}

8.6.2 点输入使用示例

public async void ExecCreateLine(string[] args = null)
{
    var pointInputer = new PointInputer(docEditor);
    
    // 获取第一个点
    commandCtrl.WriteInfo("指定第一点:");
    var result1 = await pointInputer.Execute();
    if (result1.Status != InputStatus.OK)
    {
        commandCtrl.WriteInfo("操作已取消");
        return;
    }
    
    // 设置基点(用于相对坐标)
    pointInputer.SetBasePoint(result1.Point);
    
    // 获取第二个点,提供选项
    pointInputer.SetOptions("关闭(C)", "撤销(U)");
    var result2 = await pointInputer.Execute("指定下一点 [关闭(C)/撤销(U)]:");
    
    if (result2.Status == InputStatus.OK)
    {
        // 创建直线
        CreateLine(result1.Point, result2.Point);
    }
    else if (result2.Status == InputStatus.Option)
    {
        // 处理选项
        HandleOption(result2.Option);
    }
}

8.6.3 ElementSetInputer元素选择

public class ElementSetInputer
{
    public bool isCancelled { get; private set; }
    
    public ElementSetInputer(IDocumentEditor docEditor);
    
    /// <summary>
    /// 执行元素选择
    /// </summary>
    public async Task<ElementInputResult> Execute(string prompt = null);
    
    /// <summary>
    /// 设置过滤器
    /// </summary>
    public void SetFilter(Func<LcElement, bool> filter);
}

public class ElementInputResult
{
    public object ValueX { get; set; }  // List<LcElement>
    public string Option { get; set; }
}

8.6.4 元素选择示例

public async void ExecSelectAndModify(string[] args = null)
{
    var elementInputer = new ElementSetInputer(docEditor);
    
    // 设置过滤器:只选择草坪元素
    elementInputer.SetFilter(e => e.Type == LayoutElementType.Lawn);
    
    var result = await elementInputer.Execute("请选择草坪元素:");
    
    if (elementInputer.isCancelled || result?.ValueX == null)
    {
        commandCtrl.WriteInfo("操作已取消");
        return;
    }
    
    var elements = result.ValueX as List<LcElement>;
    commandCtrl.WriteInfo($"选择了 {elements.Count} 个草坪元素");
    
    // 对选中的元素进行操作
    foreach (var ele in elements)
    {
        var lawn = ele as QdLawn;
        lawn.Bottom += 100;  // 提高底部标高
    }
}

8.7 夹点编辑

8.7.1 ControlGrip控制夹点

public class ControlGrip
{
    /// <summary>
    /// 所属元素
    /// </summary>
    public LcElement Element { get; set; }
    
    /// <summary>
    /// 夹点名称(用于识别)
    /// </summary>
    public string Name { get; set; }
    
    /// <summary>
    /// 夹点位置
    /// </summary>
    public Vector2 Position { get; set; }
    
    /// <summary>
    /// 夹点类型
    /// </summary>
    public GripType Type { get; set; }
}

public enum GripType
{
    Move,       // 移动
    Stretch,    // 拉伸
    Rotate,     // 旋转
    Scale       // 缩放
}

8.7.2 实现夹点编辑

public class MyElementAction : DirectComponentAction
{
    /// <summary>
    /// 获取元素的控制夹点
    /// </summary>
    public override ControlGrip[] GetControlGrips(LcElement element)
    {
        var myEle = element as MyElement;
        var grips = new List<ControlGrip>();
        
        // 中心移动夹点
        grips.Add(new ControlGrip
        {
            Element = myEle,
            Name = "Center",
            Position = myEle.BoundingBox.Center.Clone(),
            Type = GripType.Move
        });
        
        // 角点拉伸夹点
        var corners = GetCorners(myEle);
        for (int i = 0; i < corners.Length; i++)
        {
            grips.Add(new ControlGrip
            {
                Element = myEle,
                Name = $"Corner_{i}",
                Position = corners[i],
                Type = GripType.Stretch
            });
        }
        
        // 旋转夹点
        grips.Add(new ControlGrip
        {
            Element = myEle,
            Name = "Rotate",
            Position = myEle.BoundingBox.Center + new Vector2(0, myEle.BoundingBox.Height / 2 + 500),
            Type = GripType.Rotate
        });
        
        return grips.ToArray();
    }
    
    /// <summary>
    /// 处理夹点拖动
    /// </summary>
    public override void SetDragGrip(LcElement element, ControlGrip grip, Vector2 position, bool isEnd)
    {
        var myEle = element as MyElement;
        
        if (!isEnd)
        {
            // 拖动过程中的预览处理
            return;
        }
        
        // 拖动结束,应用修改
        var offset = position - grip.Position;
        
        switch (grip.Name)
        {
            case "Center":
                // 移动元素
                myEle.Translate(offset);
                break;
                
            case var name when name.StartsWith("Corner_"):
                // 拉伸角点
                var cornerIndex = int.Parse(name.Split('_')[1]);
                StretchCorner(myEle, cornerIndex, position);
                break;
                
            case "Rotate":
                // 旋转元素
                var center = myEle.BoundingBox.Center;
                var angle = Math.Atan2(position.Y - center.Y, position.X - center.X) - Math.PI / 2;
                myEle.Rotation = angle;
                break;
        }
        
        myEle.ResetBoundingBox();
    }
    
    /// <summary>
    /// 绘制拖动过程中的预览
    /// </summary>
    public override void DrawDragGrip(LcCanvas2d canvas)
    {
        // 可以在这里绘制拖动时的虚线预览
    }
}

8.8 捕捉系统

8.8.1 SnapPointResult捕捉结果

public class SnapPointResult
{
    /// <summary>
    /// 所属元素
    /// </summary>
    public LcElement Element { get; set; }
    
    /// <summary>
    /// 捕捉点
    /// </summary>
    public Vector2 Point { get; set; }
    
    /// <summary>
    /// 捕捉类型名称
    /// </summary>
    public string Name { get; set; }
    
    /// <summary>
    /// 参考曲线
    /// </summary>
    public List<SnapRefCurve> Curves { get; set; } = new List<SnapRefCurve>();
}

public class SnapRefCurve
{
    public SnapPointType Type { get; set; }
    public Curve2d Curve { get; set; }
    
    public SnapRefCurve(SnapPointType type, Curve2d curve)
    {
        Type = type;
        Curve = curve;
    }
}

public enum SnapPointType
{
    Endpoint,       // 端点
    Midpoint,       // 中点
    Center,         // 圆心
    Quadrant,       // 象限点
    Intersection,   // 交点
    Perpendicular,  // 垂足
    Tangent,        // 切点
    Nearest,        // 最近点
    Node            // 节点
}

8.8.2 实现捕捉

public override SnapPointResult SnapPoint(SnapRuntime snapRt, LcElement element, 
    Vector2 point, double maxDistance, bool isReturn, Matrix3 matrix)
{
    var myEle = element as MyElement;
    var snapSettings = SnapSettings.Current;
    var result = new SnapPointResult { Element = element };
    
    // 端点捕捉
    if (snapSettings.Endpoint)
    {
        foreach (var curve in myEle.GetShapes()[0].Curve2ds)
        {
            var endpoints = new[] { curve.GetStartPoint(), curve.GetEndPoint() };
            foreach (var ep in endpoints)
            {
                if (ep.DistanceTo(point) < maxDistance)
                {
                    result.Point = ep;
                    result.Name = "端点";
                    result.Curves.Add(new SnapRefCurve(SnapPointType.Endpoint, curve.Clone()));
                    return result;
                }
            }
        }
    }
    
    // 中点捕捉
    if (snapSettings.Midpoint)
    {
        foreach (var curve in myEle.GetShapes()[0].Curve2ds)
        {
            var midpoint = curve.GetMidPoint();
            if (midpoint.DistanceTo(point) < maxDistance)
            {
                result.Point = midpoint;
                result.Name = "中点";
                result.Curves.Add(new SnapRefCurve(SnapPointType.Midpoint, curve.Clone()));
                return result;
            }
        }
    }
    
    // 最近点捕捉
    if (snapSettings.Nearest)
    {
        foreach (var curve in myEle.GetShapes()[0].Curve2ds)
        {
            var nearest = curve.GetNearestPoint(point);
            if (nearest.DistanceTo(point) < maxDistance)
            {
                result.Point = nearest;
                result.Name = "最近点";
                result.Curves.Add(new SnapRefCurve(SnapPointType.Nearest, curve.Clone()));
                return result;
            }
        }
    }
    
    return null;  // 未捕捉到任何点
}

8.9 实时预览

8.9.1 创建时预览

public class RectangleAction : DirectComponentAction
{
    private Vector2 _startPoint;
    private Vector2 _currentPoint;
    private bool _isDrawing;
    
    public async void ExecCreateRectangle(string[] args = null)
    {
        var pointInputer = new PointInputer(docEditor);
        
        // 设置创建绘制器
        vportRt.SetCreateDrawer(DrawPreview);
        
        // 获取第一个角点
        commandCtrl.WriteInfo("指定第一个角点:");
        var result1 = await pointInputer.Execute();
        if (result1.Status != InputStatus.OK) goto End;
        
        _startPoint = result1.Point;
        _isDrawing = true;
        
        // 获取对角点
        commandCtrl.WriteInfo("指定对角点:");
        var result2 = await pointInputer.Execute();
        if (result2.Status != InputStatus.OK) goto End;
        
        // 创建矩形
        CreateRectangle(_startPoint, result2.Point);
        
    End:
        _isDrawing = false;
        vportRt.SetCreateDrawer(null);
    }
    
    /// <summary>
    /// 绘制创建预览
    /// </summary>
    private void DrawPreview(LcCanvas2d canvas, Matrix3 matrix)
    {
        if (!_isDrawing) return;
        
        // 获取当前鼠标位置
        _currentPoint = vportRt.GetMouseWorldPosition();
        
        var pen = new LcPaint
        {
            Color = new Color(0x0000FF),
            Width = 1,
            StrokeStyle = StrokeStyle.Dashed
        };
        
        // 绘制预览矩形
        var p1 = _startPoint.ApplyMatrix3(matrix);
        var p2 = new Vector2(_currentPoint.X, _startPoint.Y).ApplyMatrix3(matrix);
        var p3 = _currentPoint.ApplyMatrix3(matrix);
        var p4 = new Vector2(_startPoint.X, _currentPoint.Y).ApplyMatrix3(matrix);
        
        canvas.DrawLine(pen, p1, p2);
        canvas.DrawLine(pen, p2, p3);
        canvas.DrawLine(pen, p3, p4);
        canvas.DrawLine(pen, p4, p1);
    }
}

8.10 本章小结

本章详细介绍了FY_Layout的二维绘图与交互操作:

  1. 绘图系统:LcCanvas2d画布和绘图方法
  2. 画笔配置:LcPaint的各种属性和样式
  3. 文字绘制:LcTextPaint的使用方法
  4. 坐标变换:Matrix3变换矩阵的应用
  5. 用户输入:PointInputer和ElementSetInputer的使用
  6. 夹点编辑:ControlGrip的实现
  7. 捕捉系统:SnapPointResult的实现
  8. 实时预览:创建时的动态预览

掌握这些二维绘图和交互技术,是开发CAD插件的基础能力。下一章我们将学习三维渲染与模型生成。


posted @ 2026-01-31 16:02  我才是银古  阅读(2)  评论(0)    收藏  举报