二维绘图与交互操作
第八章:二维绘图与交互操作
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的二维绘图与交互操作:
- 绘图系统:LcCanvas2d画布和绘图方法
- 画笔配置:LcPaint的各种属性和样式
- 文字绘制:LcTextPaint的使用方法
- 坐标变换:Matrix3变换矩阵的应用
- 用户输入:PointInputer和ElementSetInputer的使用
- 夹点编辑:ControlGrip的实现
- 捕捉系统:SnapPointResult的实现
- 实时预览:创建时的动态预览
掌握这些二维绘图和交互技术,是开发CAD插件的基础能力。下一章我们将学习三维渲染与模型生成。

浙公网安备 33010602011771号