第十九章:调试测试与性能优化
19.1 测试框架
19.1.1 测试项目结构
test/
├── LightCAD.MathLib.Tests/ # 数学库单元测试
│ ├── LightCAD.MathLib.Tests.csproj
│ ├── Point2dTests.cs
│ ├── Vector2dTests.cs
│ ├── Matrix4dTests.cs
│ ├── IntersectionTests.cs
│ ├── BezierCurveTests.cs
│ └── NurbsCurveTests.cs
└── WinFormTest/ # 窗体功能测试
├── WinFormTest.csproj
└── ViewportTests.cs
19.1.2 测试框架选择
LightCAD使用MSTest作为单元测试框架:
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.x" />
<PackageReference Include="MSTest.TestAdapter" Version="3.x" />
<PackageReference Include="MSTest.TestFramework" Version="3.x" />
19.2 单元测试实践
19.2.1 数学库测试
[TestClass]
public class Point2dTests
{
[TestMethod]
public void Distance_Origin_To_3_4_Should_Be_5()
{
var p1 = new Point2d(0, 0);
var p2 = new Point2d(3, 4);
Assert.AreEqual(5.0, p1.DistanceTo(p2), 1e-10);
}
[TestMethod]
public void MidPoint_Should_Be_Average()
{
var p1 = new Point2d(0, 0);
var p2 = new Point2d(100, 200);
var mid = p1.MidPoint(p2);
Assert.AreEqual(50.0, mid.X, 1e-10);
Assert.AreEqual(100.0, mid.Y, 1e-10);
}
[TestMethod]
public void IsEqualTo_WithinTolerance_Should_Return_True()
{
var p1 = new Point2d(1.0, 2.0);
var p2 = new Point2d(1.0 + 1e-11, 2.0 - 1e-11);
Assert.IsTrue(p1.IsEqualTo(p2));
}
[TestMethod]
public void IsEqualTo_OutsideTolerance_Should_Return_False()
{
var p1 = new Point2d(1.0, 2.0);
var p2 = new Point2d(1.1, 2.0);
Assert.IsFalse(p1.IsEqualTo(p2));
}
}
[TestClass]
public class Matrix4dTests
{
[TestMethod]
public void Identity_Transform_Should_Not_Change_Point()
{
var point = new Point3d(10, 20, 30);
var result = Matrix4d.Identity.Transform(point);
Assert.AreEqual(10, result.X, 1e-10);
Assert.AreEqual(20, result.Y, 1e-10);
Assert.AreEqual(30, result.Z, 1e-10);
}
[TestMethod]
public void Translation_Should_Move_Point()
{
var point = new Point3d(0, 0, 0);
var matrix = Matrix4d.CreateTranslation(10, 20, 30);
var result = matrix.Transform(point);
Assert.AreEqual(10, result.X, 1e-10);
Assert.AreEqual(20, result.Y, 1e-10);
Assert.AreEqual(30, result.Z, 1e-10);
}
[TestMethod]
public void RotationZ_90Degrees_Should_Rotate_Correctly()
{
var point = new Point3d(1, 0, 0);
var matrix = Matrix4d.CreateRotationZ(Math.PI / 2);
var result = matrix.Transform(point);
Assert.AreEqual(0, result.X, 1e-10);
Assert.AreEqual(1, result.Y, 1e-10);
Assert.AreEqual(0, result.Z, 1e-10);
}
[TestMethod]
public void Inverse_Times_Original_Should_Be_Identity()
{
var matrix = Matrix4d.CreateTranslation(10, 20, 30)
* Matrix4d.CreateRotationZ(Math.PI / 4)
* Matrix4d.CreateScale(2, 3, 1);
var inverse = matrix.Inverse();
var product = matrix * inverse;
// 验证结果接近单位矩阵
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
var expected = (i == j) ? 1.0 : 0.0;
Assert.AreEqual(expected, product[i, j], 1e-10);
}
}
}
}
[TestClass]
public class IntersectionTests
{
[TestMethod]
public void LineLineIntersect_Perpendicular()
{
var result = LineLineIntersect.Compute(
new Point2d(0, 0), new Vector2d(1, 0),
new Point2d(5, -5), new Vector2d(0, 1)
);
Assert.IsNotNull(result);
Assert.AreEqual(5.0, result.Value.X, 1e-10);
Assert.AreEqual(0.0, result.Value.Y, 1e-10);
}
[TestMethod]
public void LineLineIntersect_Parallel_Returns_Null()
{
var result = LineLineIntersect.Compute(
new Point2d(0, 0), new Vector2d(1, 0),
new Point2d(0, 1), new Vector2d(1, 0)
);
Assert.IsNull(result);
}
[TestMethod]
public void LineCircleIntersect_Two_Points()
{
var results = LineCircleIntersect.Compute(
new Point2d(-10, 0), new Vector2d(1, 0),
new Point2d(0, 0), 5
);
Assert.AreEqual(2, results.Count);
}
[TestMethod]
public void LineCircleIntersect_Tangent()
{
var results = LineCircleIntersect.Compute(
new Point2d(0, 5), new Vector2d(1, 0),
new Point2d(0, 0), 5
);
Assert.AreEqual(1, results.Count);
Assert.AreEqual(0, results[0].X, 1e-6);
Assert.AreEqual(5, results[0].Y, 1e-6);
}
[TestMethod]
public void LineCircleIntersect_No_Intersection()
{
var results = LineCircleIntersect.Compute(
new Point2d(0, 10), new Vector2d(1, 0),
new Point2d(0, 0), 5
);
Assert.AreEqual(0, results.Count);
}
}
19.2.2 运行测试
# 运行所有测试
dotnet test LightCAD1.sln
# 运行特定测试项目
dotnet test test/LightCAD.MathLib.Tests/LightCAD.MathLib.Tests.csproj
# 运行特定测试类
dotnet test --filter "ClassName=LightCAD.MathLib.Tests.Point2dTests"
# 运行特定测试方法
dotnet test --filter "FullyQualifiedName~Distance_Origin_To_3_4"
# 生成代码覆盖率报告
dotnet test --collect:"XPlat Code Coverage"
19.3 调试技巧
19.3.1 断点策略
几何计算断点:
在数学库的关键计算位置设置断点:
- 交点计算结果
- 曲线参数求值
- 矩阵变换结果
渲染调试断点:
在渲染管线的关键位置设置断点:
- 几何体创建
- 材质设置
- 场景更新
输入处理断点:
在输入系统的关键位置设置断点:
- 鼠标坐标转换
- 捕捉点计算
- 命令执行
19.3.2 日志系统
public static class Logger
{
private static readonly string logDir = "Logs";
private static StreamWriter writer;
public static void Initialize()
{
Directory.CreateDirectory(logDir);
var logFile = Path.Combine(logDir,
$"lightcad_{DateTime.Now:yyyyMMdd_HHmmss}.log");
writer = new StreamWriter(logFile, true);
}
public static void Debug(string message)
{
Log("DEBUG", message);
}
public static void Info(string message)
{
Log("INFO", message);
}
public static void Warning(string message)
{
Log("WARN", message);
}
public static void Error(string message, Exception ex = null)
{
Log("ERROR", message);
if (ex != null)
{
Log("ERROR", $"Exception: {ex.Message}");
Log("ERROR", $"StackTrace: {ex.StackTrace}");
}
}
private static void Log(string level, string message)
{
var line = $"[{DateTime.Now:HH:mm:ss.fff}] [{level}] {message}";
writer?.WriteLine(line);
writer?.Flush();
System.Diagnostics.Debug.WriteLine(line);
}
}
19.3.3 可视化调试
/// <summary>
/// 调试辅助:在视口中绘制调试几何
/// </summary>
public static class DebugDraw
{
private static List<TemporaryGraphic> debugGraphics = new();
public static void DrawPoint(Point2d point, LcColor color)
{
debugGraphics.Add(new TemporaryGraphic
{
Type = TempGraphicType.Point,
Center = point,
Color = color
});
}
public static void DrawLine(Point2d start, Point2d end, LcColor color)
{
debugGraphics.Add(new TemporaryGraphic
{
Type = TempGraphicType.Line,
Points = new[] { start, end },
Color = color
});
}
public static void DrawBounds(BoundingBox2d bounds, LcColor color)
{
DrawLine(bounds.Min, new Point2d(bounds.Max.X, bounds.Min.Y), color);
DrawLine(new Point2d(bounds.Max.X, bounds.Min.Y), bounds.Max, color);
DrawLine(bounds.Max, new Point2d(bounds.Min.X, bounds.Max.Y), color);
DrawLine(new Point2d(bounds.Min.X, bounds.Max.Y), bounds.Min, color);
}
public static void Clear()
{
debugGraphics.Clear();
}
}
19.4 性能优化
19.4.1 内存优化
对象池:
public class ObjectPool<T> where T : new()
{
private readonly ConcurrentBag<T> pool = new();
private readonly int maxSize;
public ObjectPool(int maxSize = 1000)
{
this.maxSize = maxSize;
}
public T Rent()
{
return pool.TryTake(out var item) ? item : new T();
}
public void Return(T item)
{
if (pool.Count < maxSize)
{
pool.Add(item);
}
}
}
// 使用示例:复用Point2d列表
var pointListPool = new ObjectPool<List<Point2d>>();
var points = pointListPool.Rent();
try
{
// 使用points...
}
finally
{
points.Clear();
pointListPool.Return(points);
}
19.4.2 渲染优化
批量渲染:
public class BatchRenderer
{
/// <summary>
/// 将多条线段合并为单次绘制调用
/// </summary>
public void DrawLinesBatch(List<LcLine> lines, LcColor color)
{
var positions = new float[lines.Count * 6]; // 每条线2个点×3坐标
for (int i = 0; i < lines.Count; i++)
{
positions[i * 6 + 0] = (float)lines[i].StartPoint.X;
positions[i * 6 + 1] = (float)lines[i].StartPoint.Y;
positions[i * 6 + 2] = 0;
positions[i * 6 + 3] = (float)lines[i].EndPoint.X;
positions[i * 6 + 4] = (float)lines[i].EndPoint.Y;
positions[i * 6 + 5] = 0;
}
var geometry = new BufferGeometry();
geometry.SetAttribute("position",
new BufferAttribute(positions, 3));
var material = new LineBasicMaterial
{
Color = ConvertColor(color)
};
scene.Add(new LineSegments(geometry, material));
}
}
空间索引:
public class SpatialIndex
{
private QuadTree<LcEntity> quadTree;
public SpatialIndex(BoundingBox2d worldBounds)
{
quadTree = new QuadTree<LcEntity>(worldBounds, maxDepth: 8);
}
public void Insert(LcEntity entity)
{
quadTree.Insert(entity, entity.Bounds);
}
public void Remove(LcEntity entity)
{
quadTree.Remove(entity);
}
/// <summary>
/// 范围查询(比遍历所有实体快得多)
/// </summary>
public IEnumerable<LcEntity> Query(BoundingBox2d searchBounds)
{
return quadTree.Query(searchBounds);
}
}
19.4.3 性能分析
public class PerformanceMonitor
{
private Dictionary<string, List<long>> timings = new();
private Stopwatch stopwatch = new();
/// <summary>
/// 计时开始
/// </summary>
public IDisposable MeasureTime(string operationName)
{
return new TimeMeasurement(this, operationName);
}
private class TimeMeasurement : IDisposable
{
private PerformanceMonitor monitor;
private string name;
private long startTicks;
public TimeMeasurement(PerformanceMonitor monitor, string name)
{
this.monitor = monitor;
this.name = name;
this.startTicks = Stopwatch.GetTimestamp();
}
public void Dispose()
{
var elapsed = Stopwatch.GetTimestamp() - startTicks;
var elapsedMs = elapsed * 1000 / Stopwatch.Frequency;
if (!monitor.timings.ContainsKey(name))
monitor.timings[name] = new List<long>();
monitor.timings[name].Add(elapsedMs);
}
}
/// <summary>
/// 输出性能报告
/// </summary>
public void PrintReport()
{
foreach (var (name, times) in timings)
{
var avg = times.Average();
var max = times.Max();
var min = times.Min();
Console.WriteLine(
$"{name}: 平均={avg:F2}ms, 最大={max}ms, 最小={min}ms, " +
$"调用次数={times.Count}");
}
}
}
// 使用示例
var monitor = new PerformanceMonitor();
using (monitor.MeasureTime("渲染场景"))
{
viewportRenderer.Render(viewport, document);
}
using (monitor.MeasureTime("视图构建"))
{
viewBuilder.BuildView(buildParams);
}
monitor.PrintReport();
19.5 常见问题排查
19.5.1 渲染问题
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
| 图元不显示 | 图层被冻结/关闭 | 检查图层状态 |
| 3D模型黑色 | 法线方向错误 | 重新计算法线 |
| 闪烁 | Z-fighting | 增加深度偏移 |
| 性能低 | 实体过多 | 启用LOD/空间索引 |
19.5.2 几何计算问题
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
| 交点丢失 | 容差设置不当 | 调整Tolerance值 |
| 布尔运算失败 | 退化几何体 | 检查输入有效性 |
| 变换结果错误 | 矩阵乘法顺序 | 检查变换组合顺序 |
19.6 本章小结
本章介绍了LightCAD的测试、调试和性能优化方法。单元测试覆盖了数学库的核心功能,确保几何计算的正确性。调试技巧包括断点策略、日志系统和可视化调试。性能优化涵盖了对象池、批量渲染、空间索引等技术。性能监控工具帮助开发者识别和解决性能瓶颈。
上一章:第十八章:标注与文本系统
下一章:第二十章:实战案例与二次开发指南