第十六章:调试测试与性能优化
16.1 开发调试概述
16.1.1 调试环境配置
FY_Layout基于Visual Studio 2022开发,其调试配置在launchSettings.json中定义:
{
"profiles": {
"QdLayout": {
"commandName": "Executable",
"executablePath": "$(SolutionDir)\\飞扬主程序\\FeiYang.exe",
"workingDirectory": "$(SolutionDir)\\飞扬主程序",
"commandLineArgs": ""
}
}
}
调试时,QdLayout项目会编译为DLL插件,由飞扬主程序(FeiYang.exe)加载运行。
16.1.2 调试输出路径
在QdLayout.csproj中配置了统一的输出路径:
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<BaseOutputPath>..\Build</BaseOutputPath>
</PropertyGroup>
所有编译输出都放在解决方案根目录的Build文件夹中,方便飞扬主程序加载。
16.2 断点调试技巧
16.2.1 插件加载调试
在LayoutPlugin.cs的Loaded方法中设置断点,可以跟踪插件的初始化过程:
public class LayoutPlugin : ILcPlugin
{
public void Loaded()
{
// 在此处设置断点,检查元素类型注册
LcDocument.RegistElementTypes(LayoutElementType.All);
// 检查Action注册
LcDocument.ElementActions.Add(LayoutElementType.Lawn, new LawnAction());
LcDocument.ElementActions.Add(LayoutElementType.Fence, new FenceAction());
// ...
// 检查3D Action注册
LcDocument.Element3dActions.Add(LayoutElementType.Lawn, new Lawn3dAction());
// ...
}
public void InitUI()
{
// 在此处设置断点,检查UI工具栏初始化
}
public void Completed()
{
// 插件初始化完成回调
}
}
16.2.2 命令执行调试
在命令方法中设置断点,跟踪用户操作的执行流程:
[CommandMethod(Name = "Lawn", ShortCuts = "LW")]
public CommandResult DrawLawn(IDocumentEditor docEditor, string[] args)
{
// 断点1:检查命令参数
var lawnAction = new LawnAction(docEditor);
// 断点2:跟踪创建过程
lawnAction.ExecCreatePoly(args);
return CommandResult.Succ();
}
16.2.3 异步方法调试
FY_Layout的Action类大量使用async/await模式,调试异步方法需要注意:
public async void ExecCreatePoly(string[] args = null)
{
OutLoop = null;
commandCtrl.WriteInfo("绘制草坪轮廓中...");
// 断点:进入多段线绘制
var plAc = new PolyLineAction(docEditor);
await plAc.StartCreating();
// 断点:用户完成绘制后的回调
if (!await plAc.OtherActionCreating())
{
goto End;
}
else
{
// 断点:获取用户绘制的轮廓
OutLoop = plAc.CurrentPoly;
}
// 断点:创建元素
CreateLawn();
End:
if (OutLoop != null)
{
vportRt.ActiveElementSet.RemoveElement(OutLoop);
}
plAc.EndCreating();
EndCreating();
}
异步调试注意事项:
- 在await前后设置断点,可以观察异步执行的切换点
- VS2022的”异步调用堆栈”窗口可以查看完整的异步调用链
- 使用”任务窗口”可以监控所有活动的异步任务
16.2.4 Provider调试
Provider的调试需要在三维视图切换时触发:
internal static Curve2dGroupCollection 草坪(
LcParameterSet pset, ShapeCreator creator)
{
// 断点:检查参数集
var com = creator.ComIns as DirectComponent;
var outline = com.BaseCurve as Polyline2d;
// 断点:检查轮廓数据
var curves = outline.Curve2ds.Clone();
var baseCurveGrp = new Curve2dGroup {
Curve2ds = curves.ToListEx()
};
return new Curve2dGroupCollection { baseCurveGrp };
}
private static Solid3dCollection GetSolid_草坪(
LcComponentDefinition definition,
LcParameterSet pset,
SolidCreator creator)
{
// 断点:检查三维生成参数
var outline = pset.GetValue<Polyline2d>("Outline");
var bottom = pset.GetValue<double>("Bottom");
// 断点:检查几何体生成
var platgeo = CreateLawn(outline);
platgeo.translate(0, 0, bottom);
// 断点:检查输出数据
return new Solid3dCollection() { ... };
}
16.3 日志与诊断
16.3.1 命令控制台输出
FY_Layout使用commandCtrl进行命令行输出:
public LawnAction(IDocumentEditor docEditor) : base(docEditor)
{
// 输出命令信息到控制台
commandCtrl.WriteInfo("命令:Lawn");
}
public async void ExecCreatePoly(string[] args = null)
{
// 输出操作状态
commandCtrl.WriteInfo("绘制草坪轮廓中...");
// ... 操作逻辑 ...
// 输出完成信息
commandCtrl.WriteInfo("草坪创建完成");
}
16.3.2 自定义日志工具
// 自定义日志工具类
public static class LayoutLogger
{
private static readonly string LogPath = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory, "Logs", "layout.log");
public static void Info(string message)
{
WriteLog("INFO", message);
}
public static void Error(string message, Exception ex = null)
{
WriteLog("ERROR", message);
if (ex != null)
{
WriteLog("ERROR", $"Exception: {ex.Message}");
WriteLog("ERROR", $"StackTrace: {ex.StackTrace}");
}
}
public static void Debug(string message)
{
#if DEBUG
WriteLog("DEBUG", message);
#endif
}
private static void WriteLog(string level, string message)
{
try
{
var dir = Path.GetDirectoryName(LogPath);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
var logLine = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] " +
$"[{level}] {message}";
File.AppendAllText(LogPath, logLine + Environment.NewLine);
}
catch { /* 日志写入失败不影响主流程 */ }
}
}
16.3.3 性能计时
// 使用Stopwatch进行性能计时
public void CreateLawn()
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var doc = docRt.Document;
var lawnDef = docRt.GetUseComDef(
$"{NamespaceKey}.绿色文明", "草坪", null) as QdLawnDef;
var lawn = new QdLawn(lawnDef);
lawn.Initilize(doc);
sw.Stop();
LayoutLogger.Debug($"草坪初始化耗时: {sw.ElapsedMilliseconds}ms");
sw.Restart();
var poly = OutLoop.Clone() as LcPolyLine;
lawn.Outline = poly.Curve.Clone() as Polyline2d;
lawn.ResetBoundingBox();
sw.Stop();
LayoutLogger.Debug($"几何数据处理耗时: {sw.ElapsedMilliseconds}ms");
lawn.Layer = GetLayer().Name;
lawn.Bottom = 0;
lawn.Material = MaterialManager.GetMaterial(MaterialManager.LawnUuid);
vportRt.ActiveElementSet.InsertElement(lawn);
docRt.Action.ClearSelects();
}
16.4 常见错误排查
16.4.1 插件加载失败
症状:飞扬主程序启动后看不到”场布”工具栏
排查步骤:
- 检查DLL输出路径是否正确(Build目录)
- 检查所有依赖库是否完整(Libs目录下的DLL)
- 检查ILcPlugin接口实现是否完整
- 查看飞扬主程序的日志输出
// 确保ILcPlugin的5个方法都已实现
public class LayoutPlugin : ILcPlugin
{
public void Loaded() { } // ✅ 必须实现
public void InitUI() { } // ✅ 必须实现
public void Completed() { } // ✅ 必须实现
public void OnInitializeDocRt(DocumentRuntime docRt) { } // ✅ 必须实现
public void OnDisposeDocRt(DocumentRuntime docRt) { } // ✅ 必须实现
}
16.4.2 元素创建失败
症状:执行命令后没有创建元素,或创建的元素不可见
排查清单:
□ 元素类型是否已注册到LayoutElementType.All数组
□ ElementAction是否已添加到LcDocument.ElementActions
□ 元素的Layer是否存在且可见
□ 元素的BoundingBox是否正确计算
□ 元素的BaseCurve/Outline是否有有效的几何数据
□ ResetBoundingBox()是否被调用
□ InsertElement()是否被调用
16.4.3 三维显示异常
症状:切换到三维视图时元素不显示或显示错误
排查步骤:
- 检查Element3dAction是否已注册
- 检查Provider是否已在RegistProviders中注册
- 检查GeometryData的顶点和索引数据是否正确
- 检查材质是否正确获取
// 常见三维数据问题
// 1. 顶点数组长度不是3的倍数
if (vertices.Length % 3 != 0)
throw new Exception("顶点数组长度必须是3的倍数");
// 2. 索引超出顶点范围
var maxIndex = vertices.Length / 3 - 1;
if (indices.Any(i => i > maxIndex))
throw new Exception("索引超出顶点范围");
// 3. 法线方向错误导致面片不可见
// 确保三角面片的顶点顺序一致(逆时针为正面)
// 4. 材质索引与Groups不匹配
if (groups.Any(g => g.MaterialIndex >= materials.Length))
throw new Exception("材质索引超出范围");
16.4.4 坐标计算错误
症状:元素位置偏移或尺寸不正确
// 常见坐标问题排查
// 1. 检查单位一致性(FY_Layout使用毫米)
// 2. 检查坐标系方向(Y轴朝上)
// 3. 检查矩阵变换顺序
// 验证包围盒
var box = element.GetBoundingBox();
LayoutLogger.Debug($"BoundingBox: " +
$"Min({box.Min.X}, {box.Min.Y}), " +
$"Max({box.Max.X}, {box.Max.Y})");
// 验证轮廓闭合性
var outline = element.Outline;
var firstPoint = outline.Curve2ds.First().GetPoints(1).First();
var lastPoint = outline.Curve2ds.Last().GetPoints(1).Last();
var isClosed = firstPoint.Similarity(lastPoint, 0);
LayoutLogger.Debug($"轮廓闭合: {isClosed}");
16.5 性能优化策略
16.5.1 二维绘图性能
问题:大量场布元素时,二维视图缩放和平移变慢
优化策略:
// 1. 包围盒裁剪 - 只绘制视口内的元素
public override void Draw(LcCanvas2d canvas, LcElement element, Matrix3 matrix)
{
var lawn = element as QdLawn;
// 快速判断是否在视口内
var viewBox = canvas.GetViewBox();
if (!viewBox.IntersectsBox(lawn.BoundingBox))
return; // 不在视口内,跳过绘制
var pen = GetDrawPen(lawn);
DrawLawn(canvas, lawn, matrix, pen);
}
// 2. 简化绘制 - 缩小到一定比例时使用简化绘制
public void DrawLawn(LcCanvas2d canvas, QdLawn lawn,
Matrix3 matrix, LcPaint pen)
{
var zoom = canvas.GetZoomLevel();
if (zoom < 0.1)
{
// 极小缩放级别时,只绘制包围盒
canvas.DrawRect(pen, lawn.BoundingBox, matrix);
return;
}
// 正常绘制
for (int i = 0; i < lawn.GetShapes()[0].Curve2ds.Count; i++)
{
canvas.DrawCurve(pen, lawn.GetShapes()[0].Curve2ds[i], matrix);
}
// 仅在足够大的缩放级别时绘制文字
if (zoom > 0.5)
{
LcTextPaint textPaint = new LcTextPaint
{
Color = new Color().Set(this.GetLayer().Color),
FontName = "仿宋",
Size = 800,
WidthFactor = 1
};
canvas.DrawText(textPaint, "草坪", new Matrix3(), out var charBoxs);
}
}
16.5.2 三维渲染性能
问题:复杂场景的三维渲染帧率低
优化策略:
// 1. LOD(细节层次)策略
public Object3D Get3dObject(LcElement element, DocumentRuntime docRt)
{
var lawn = element as QdLawn;
var distance = CalculateDistanceToCamera(lawn);
if (distance > 100000) // 100m以外
{
// 简化模型 - 只生成平面
return CreateSimplePlane(lawn);
}
else
{
// 完整模型
return CreateDetailedModel(lawn);
}
}
// 2. 几何体合并 - 减少Draw Call
public static BufferGeometry MergeGeometries(
List<BufferGeometry> geometries)
{
var totalVertices = geometries.Sum(g =>
g.attributes.position.array.Length);
var totalIndices = geometries.Sum(g =>
g.index.intArray.Length);
var mergedVertices = new double[totalVertices];
var mergedIndices = new int[totalIndices];
int vertexOffset = 0;
int indexOffset = 0;
int indexValueOffset = 0;
foreach (var geo in geometries)
{
Array.Copy(geo.attributes.position.array, 0,
mergedVertices, vertexOffset,
geo.attributes.position.array.Length);
for (int i = 0; i < geo.index.intArray.Length; i++)
{
mergedIndices[indexOffset + i] =
geo.index.intArray[i] + indexValueOffset;
}
vertexOffset += geo.attributes.position.array.Length;
indexOffset += geo.index.intArray.Length;
indexValueOffset += geo.attributes.position.array.Length / 3;
}
// 创建合并后的几何体
var merged = new BufferGeometry();
merged.setAttribute("position",
new BufferAttribute(mergedVertices, 3));
merged.setIndex(new BufferAttribute(mergedIndices, 1));
return merged;
}
// 3. 缓存策略 - 避免重复生成
private static Dictionary<string, BufferGeometry> geometryCache
= new Dictionary<string, BufferGeometry>();
private static BufferGeometry GetOrCreateGeometry(
string key, Func<BufferGeometry> creator)
{
if (!geometryCache.ContainsKey(key))
{
geometryCache[key] = creator();
}
return geometryCache[key];
}
16.5.3 内存优化
// 1. 及时释放临时对象
public void CreateLawn()
{
// 使用using确保临时资源释放
var poly = OutLoop.Clone() as LcPolyLine;
try
{
var lawn = new QdLawn(lawnDef);
lawn.Initilize(doc);
lawn.Outline = poly.Curve.Clone() as Polyline2d;
// ...
}
finally
{
// 清理临时多段线
if (OutLoop != null)
{
vportRt.ActiveElementSet.RemoveElement(OutLoop);
OutLoop = null;
}
}
}
// 2. 大型集合使用Span/Memory减少分配
// 3. 几何缓存使用弱引用
private static ConditionalWeakTable<string, BufferGeometry> weakCache
= new ConditionalWeakTable<string, BufferGeometry>();
// 4. 元素的ResetCache方法清理缓存
public override void ResetCache()
{
base.ResetCache();
shapes = null; // 清理Shape缓存
}
16.5.4 响应性优化
// 使用async/await保持UI响应
public async void ExecCreatePoly(string[] args = null)
{
// 在后台线程处理耗时操作
await Task.Run(() =>
{
// 复杂的几何计算
var processedOutline = ProcessOutline(OutLoop);
});
// 在UI线程更新界面
CreateLawn();
}
16.6 单元测试建议
16.6.1 几何计算测试
// 测试轮廓偏移算法
[TestMethod]
public void TestShapeExtend_Rectangle()
{
// 创建一个矩形轮廓
var curves = new List<Curve2d>
{
new Line2d(new Vector2(0, 0), new Vector2(100, 0)),
new Line2d(new Vector2(100, 0), new Vector2(100, 100)),
new Line2d(new Vector2(100, 100), new Vector2(0, 100)),
new Line2d(new Vector2(0, 100), new Vector2(0, 0))
};
// 向外偏移10
var result = QdFoundationPitProvider.ShapeExtend(curves, 10);
// 验证偏移后的矩形
Assert.AreEqual(4, result.Count);
// 偏移后应该更大
var bounds = new Box2().ExpandByPoints(
result.SelectMany(c => c.GetPoints()).ToArray());
Assert.IsTrue(bounds.Width > 100);
Assert.IsTrue(bounds.Height > 100);
}
// 测试曲线闭合检测
[TestMethod]
public void TestCheckLoops_ClosedSquare()
{
// 创建四条线段组成正方形
var lines = new List<LcCurve2d>();
// ... 创建测试数据 ...
var result = LcCurveChangeLoop.CheckLoops(lines);
Assert.AreEqual(1, result.Count);
Assert.IsTrue(result[0].Curve.IsClosed);
}
// 测试元素包围盒
[TestMethod]
public void TestLawnBoundingBox()
{
// 创建草坪元素
var lawn = CreateTestLawn(0, 0, 100, 100);
var box = lawn.GetBoundingBox();
Assert.AreEqual(0, box.Min.X, 0.001);
Assert.AreEqual(0, box.Min.Y, 0.001);
Assert.AreEqual(100, box.Max.X, 0.001);
Assert.AreEqual(100, box.Max.Y, 0.001);
}
16.6.2 Provider测试
// 测试三维模型生成
[TestMethod]
public void TestLawnProvider_GeneratesSolid()
{
// 创建参数集
var pset = new LcParameterSet();
pset.SetValue("Outline", CreateTestOutline());
pset.SetValue("Bottom", 0.0);
// 生成三维数据
var solids = GetSolid_草坪(null, pset, null);
Assert.AreEqual(1, solids.Count);
Assert.IsNotNull(solids[0].Geometry);
Assert.IsTrue(solids[0].Geometry.Verteics.Length > 0);
Assert.IsTrue(solids[0].Geometry.Indics.Length > 0);
Assert.IsTrue(solids[0].Geometry.Indics.Length % 3 == 0); // 三角面
}
// 测试基坑多材质
[TestMethod]
public void TestFoundationPitProvider_MultipleMaterials()
{
var pset = CreateFoundationPitParams();
var solids = GetSolid_基坑(null, pset, null);
// 基坑应生成2个实体(坑底+坡面)
Assert.AreEqual(2, solids.Count);
Assert.AreEqual("Pit", solids[0].Name);
Assert.AreEqual("Slope", solids[1].Name);
}
16.7 本章小结
本章全面介绍了FY_Layout的调试、测试和性能优化方法:
- 调试环境:通过Visual Studio 2022配合飞扬主程序进行调试,支持断点、异步调试、Provider调试等
- 日志系统:使用commandCtrl输出命令信息,自定义LayoutLogger进行详细日志记录
- 错误排查:系统化的排查清单,覆盖插件加载、元素创建、三维显示、坐标计算等常见问题
- 性能优化:从二维绘图、三维渲染、内存管理、响应性四个维度的优化策略
- 单元测试:几何计算、Provider输出、元素属性的测试建议
调试和优化是保证FY_Layout插件质量的关键环节,掌握这些技能可以大幅提升开发效率和用户体验。
| 上一章:文件格式与数据交换 | 下一章:项目部署与发布指南 |