znlgis 博客

GIS开发与技术分享

第十六章:调试测试与性能优化

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 插件加载失败

症状:飞扬主程序启动后看不到”场布”工具栏

排查步骤

  1. 检查DLL输出路径是否正确(Build目录)
  2. 检查所有依赖库是否完整(Libs目录下的DLL)
  3. 检查ILcPlugin接口实现是否完整
  4. 查看飞扬主程序的日志输出
// 确保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 三维显示异常

症状:切换到三维视图时元素不显示或显示错误

排查步骤

  1. 检查Element3dAction是否已注册
  2. 检查Provider是否已在RegistProviders中注册
  3. 检查GeometryData的顶点和索引数据是否正确
  4. 检查材质是否正确获取
// 常见三维数据问题
// 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的调试、测试和性能优化方法:

  1. 调试环境:通过Visual Studio 2022配合飞扬主程序进行调试,支持断点、异步调试、Provider调试等
  2. 日志系统:使用commandCtrl输出命令信息,自定义LayoutLogger进行详细日志记录
  3. 错误排查:系统化的排查清单,覆盖插件加载、元素创建、三维显示、坐标计算等常见问题
  4. 性能优化:从二维绘图、三维渲染、内存管理、响应性四个维度的优化策略
  5. 单元测试:几何计算、Provider输出、元素属性的测试建议

调试和优化是保证FY_Layout插件质量的关键环节,掌握这些技能可以大幅提升开发效率和用户体验。


上一章:文件格式与数据交换 下一章:项目部署与发布指南