using System; using System.Collections.Generic; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace NavisworksTransport.UnitTests.Core { #region 简单的几何类型(不依赖 Navisworks API) /// /// 简单的 3D 点结构体(用于测试) /// public struct TestPoint3D : IEquatable { public double X { get; set; } public double Y { get; set; } public double Z { get; set; } public TestPoint3D(double x, double y, double z) { X = x; Y = y; Z = z; } public static TestPoint3D operator +(TestPoint3D p, TestVector3D v) { return new TestPoint3D(p.X + v.X, p.Y + v.Y, p.Z + v.Z); } public static TestPoint3D operator -(TestPoint3D p1, TestPoint3D p2) { return new TestPoint3D(p1.X - p2.X, p1.Y - p2.Y, p1.Z - p2.Z); } public static TestPoint3D operator *(TestPoint3D p, double scalar) { return new TestPoint3D(p.X * scalar, p.Y * scalar, p.Z * scalar); } public double Length { get { return Math.Sqrt(X * X + Y * Y + Z * Z); } } public bool Equals(TestPoint3D other) { return Math.Abs(X - other.X) < 1e-10 && Math.Abs(Y - other.Y) < 1e-10 && Math.Abs(Z - other.Z) < 1e-10; } public override bool Equals(object obj) { return obj is TestPoint3D && Equals((TestPoint3D)obj); } public override int GetHashCode() { return X.GetHashCode() ^ Y.GetHashCode() ^ Z.GetHashCode(); } public static bool operator ==(TestPoint3D p1, TestPoint3D p2) { return p1.Equals(p2); } public static bool operator !=(TestPoint3D p1, TestPoint3D p2) { return !p1.Equals(p2); } } /// /// 简单的 3D 向量结构体(用于测试) /// public struct TestVector3D { public double X { get; set; } public double Y { get; set; } public double Z { get; set; } public TestVector3D(double x, double y, double z) { X = x; Y = y; Z = z; } public TestVector3D(TestPoint3D point) { X = point.X; Y = point.Y; Z = point.Z; } public static TestVector3D operator +(TestVector3D v1, TestVector3D v2) { return new TestVector3D(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z); } public static TestVector3D operator -(TestVector3D v1, TestVector3D v2) { return new TestVector3D(v1.X - v2.X, v1.Y - v2.Y, v1.Z - v2.Z); } public static TestVector3D operator *(TestVector3D v, double scalar) { return new TestVector3D(v.X * scalar, v.Y * scalar, v.Z * scalar); } public static TestVector3D operator *(double scalar, TestVector3D v) { return v * scalar; } public double Length { get { return Math.Sqrt(X * X + Y * Y + Z * Z); } } public TestVector3D Normalize() { double len = Length; if (len < 1e-10) return new TestVector3D(0, 0, 0); return new TestVector3D(X / len, Y / len, Z / len); } public static double DotProduct(TestVector3D v1, TestVector3D v2) { return v1.X * v2.X + v1.Y * v2.Y + v1.Z * v2.Z; } public static TestVector3D CrossProduct(TestVector3D v1, TestVector3D v2) { return new TestVector3D( v1.Y * v2.Z - v1.Z * v2.Y, v1.Z * v2.X - v1.X * v2.Z, v1.X * v2.Y - v1.Y * v2.X ); } } /// /// 测试用圆弧轨迹数据 /// public class TestArcTrajectory { public TestPoint3D Ts { get; set; } // 进入切点 public TestPoint3D Te { get; set; } // 退出切点 public TestPoint3D ArcCenter { get; set; } // 圆心 public double RequestedRadius { get; set; } // 请求半径 public double ActualRadius { get; set; } // 实际半径 public double DeflectionAngle { get; set; } // 偏转角(弧度) public double ArcLength { get; set; } // 圆弧长度 } #endregion /// /// PathCurveEngine 独立测试版本(不依赖 Navisworks API) /// 用于测试核心算法逻辑的正确性 /// public static class TestPathCurveEngine { private static double Clamp(double value, double min, double max) { if (value < min) return min; if (value > max) return max; return value; } /// /// 计算圆弧切点和轨迹参数 /// public static TestArcTrajectory CalculateFillet( TestPoint3D pPrev, TestPoint3D pCurr, TestPoint3D pNext, double turnRadius) { // 1. 计算单位向量 TestVector3D v1 = new TestVector3D(pPrev - pCurr).Normalize(); TestVector3D v2 = new TestVector3D(pNext - pCurr).Normalize(); // 2. 计算夹角 α double cosAlpha = TestVector3D.DotProduct(v1, v2); double angleRad = Math.Acos(Clamp(cosAlpha, -1.0, 1.0)); // 如果几乎共线,返回无效轨迹 if (angleRad < 0.01 || angleRad > Math.PI - 0.01) { return new TestArcTrajectory { Ts = pCurr, Te = pCurr, ArcCenter = pCurr, RequestedRadius = turnRadius, ActualRadius = 0, DeflectionAngle = angleRad, ArcLength = 0 }; } // 3. 计算切线长 Lt = R / tan(α/2) double Lt = turnRadius / Math.Tan(angleRad / 2.0); // 4. 安全截断检查(限制为边长的45%) double seg1Length = (pPrev - pCurr).Length; double seg2Length = (pNext - pCurr).Length; double maxAllowedLt = Math.Min(seg1Length, seg2Length) * 0.45; double actualRadius = turnRadius; if (Lt > maxAllowedLt) { Lt = maxAllowedLt; actualRadius = Lt * Math.Tan(angleRad / 2.0); } // 5. 计算切点 TestPoint3D ts = pCurr + v1 * Lt; TestPoint3D te = pCurr + v2 * Lt; // 6. 计算圆心(使用角平分线) TestVector3D bisector = (v1 + v2).Normalize(); double distToCenter = actualRadius / Math.Sin(angleRad / 2.0); TestPoint3D arcCenter = pCurr + bisector * distToCenter; // 7. 计算圆弧长度 double arcLength = actualRadius * angleRad; return new TestArcTrajectory { Ts = ts, Te = te, ArcCenter = arcCenter, RequestedRadius = turnRadius, ActualRadius = actualRadius, DeflectionAngle = angleRad, ArcLength = arcLength }; } /// /// 采样圆弧为离散点序列 /// public static List SampleArc(TestArcTrajectory trajectory, double samplingStep) { var points = new List(); if (trajectory.ActualRadius < 1e-10 || trajectory.ArcLength < 1e-10) { points.Add(trajectory.Ts); points.Add(trajectory.Te); return points; } // 计算采样点数量 int numSamples = Math.Max(2, (int)Math.Ceiling(trajectory.ArcLength / samplingStep)); // 计算起始和结束角度 TestVector3D vStart = new TestVector3D(trajectory.Ts - trajectory.ArcCenter).Normalize(); TestVector3D vEnd = new TestVector3D(trajectory.Te - trajectory.ArcCenter).Normalize(); // 计算旋转轴(使用叉积) TestVector3D axis = TestVector3D.CrossProduct(vStart, vEnd).Normalize(); // 生成采样点 for (int i = 0; i <= numSamples; i++) { double t = (double)i / numSamples; double angle = t * trajectory.DeflectionAngle; TestPoint3D p = RotatePointAroundAxis(trajectory.Ts, trajectory.ArcCenter, axis, angle); points.Add(p); } // 确保最后一个点是退出切点 points[points.Count - 1] = trajectory.Te; return points; } /// /// 绕轴旋转点(罗德里格斯旋转公式) /// private static TestPoint3D RotatePointAroundAxis( TestPoint3D point, TestPoint3D center, TestVector3D axis, double angle) { TestVector3D v = new TestVector3D(point - center); TestVector3D kxv = TestVector3D.CrossProduct(axis, v); double kdv = TestVector3D.DotProduct(axis, v); TestVector3D vRot = v * Math.Cos(angle) + kxv * Math.Sin(angle) + axis * kdv * (1 - Math.Cos(angle)); return center + vRot; } } /// /// PathCurveEngine 核心算法独立测试 /// 测试路径曲线化算法的正确性(不依赖 Navisworks API) /// [TestClass] public class PathCurveEngineStandaloneTests { #region CalculateFillet 测试 [TestMethod] public void Standalone_CalculateFillet_RightAngleTurn_ReturnsValidArc() { // Arrange - 创建90度直角转弯 var pPrev = new TestPoint3D(0, 10, 0); // 上方点 var pCurr = new TestPoint3D(0, 0, 0); // 转弯点 var pNext = new TestPoint3D(10, 0, 0); // 右方点 double turnRadius = 2.0; // Act var trajectory = TestPathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); // Assert Assert.IsNotNull(trajectory, "轨迹不应该为null"); Assert.AreEqual(turnRadius, trajectory.RequestedRadius, 0.01, "请求半径应该正确"); Assert.IsTrue(trajectory.ActualRadius > 0, "实际半径应该大于0"); Assert.IsTrue(trajectory.DeflectionAngle > 0, "偏转角应该大于0"); Assert.IsTrue(trajectory.ArcLength > 0, "圆弧长度应该大于0"); // 验证切点位置 Assert.IsTrue(trajectory.Ts.Y > 0, "进入切点应该在转弯点上方"); Assert.IsTrue(Math.Abs(trajectory.Ts.X) < 0.01, "进入切点X坐标应该接近0"); Assert.IsTrue(trajectory.Te.X > 0, "退出切点应该在转弯点右侧"); Assert.IsTrue(Math.Abs(trajectory.Te.Y) < 0.01, "退出切点Y坐标应该接近0"); } [TestMethod] public void Standalone_CalculateFillet_AcuteAngleTurn_ReturnsValidArc() { // Arrange - 创建锐角转弯(约60度) var pPrev = new TestPoint3D(0, 10, 0); var pCurr = new TestPoint3D(0, 0, 0); var pNext = new TestPoint3D(8.66, 5, 0); // 60度方向 double turnRadius = 2.0; // Act var trajectory = TestPathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); // Assert Assert.IsNotNull(trajectory, "轨迹不应该为null"); Assert.IsTrue(trajectory.ActualRadius > 0, "实际半径应该大于0"); Assert.IsTrue(trajectory.DeflectionAngle > 0.5 && trajectory.DeflectionAngle < 1.5, "偏转角应该在60度左右"); } [TestMethod] public void Standalone_CalculateFillet_ObtuseAngleTurn_ReturnsValidArc() { // Arrange - 创建钝角转弯(约120度) var pPrev = new TestPoint3D(0, 10, 0); var pCurr = new TestPoint3D(0, 0, 0); var pNext = new TestPoint3D(8.66, -5, 0); // 120度方向 double turnRadius = 2.0; // Act var trajectory = TestPathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); // Assert Assert.IsNotNull(trajectory, "轨迹不应该为null"); Assert.IsTrue(trajectory.ActualRadius > 0, "实际半径应该大于0"); Assert.IsTrue(trajectory.DeflectionAngle > 1.5 && trajectory.DeflectionAngle < 2.5, "偏转角应该在120度左右"); } [TestMethod] public void Standalone_CalculateFillet_CollinearPoints_ReturnsInvalidArc() { // Arrange - 创建共线点(几乎直线) var pPrev = new TestPoint3D(0, 10, 0); var pCurr = new TestPoint3D(0, 0, 0); var pNext = new TestPoint3D(0, -10, 0); // 同一直线 double turnRadius = 2.0; // Act var trajectory = TestPathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); // Assert Assert.IsNotNull(trajectory, "轨迹不应该为null"); Assert.AreEqual(0, trajectory.ActualRadius, 0.01, "实际半径应该为0"); Assert.AreEqual(0, trajectory.ArcLength, 0.01, "圆弧长度应该为0"); Assert.AreEqual(pCurr, trajectory.Ts, "切点应该与转弯点相同"); } [TestMethod] public void Standalone_CalculateFillet_SafetyTruncation_AdjustsRadius() { // Arrange - 创建短边场景,需要安全截断 var pPrev = new TestPoint3D(0, 1, 0); // 短边 var pCurr = new TestPoint3D(0, 0, 0); var pNext = new TestPoint3D(1, 0, 0); // 短边 double turnRadius = 2.0; // 半径大于边长 // Act var trajectory = TestPathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); // Assert Assert.IsNotNull(trajectory, "轨迹不应该为null"); Assert.IsTrue(trajectory.ActualRadius < turnRadius, "实际半径应该小于请求半径"); Assert.IsTrue(trajectory.ActualRadius > 0, "实际半径应该大于0"); } [TestMethod] public void Standalone_CalculateFillet_3DTurn_ReturnsValidArc() { // Arrange - 创建3D空间中的转弯 var pPrev = new TestPoint3D(0, 10, 0); var pCurr = new TestPoint3D(0, 0, 0); var pNext = new TestPoint3D(10, 0, 5); // 有Z轴变化 double turnRadius = 2.0; // Act var trajectory = TestPathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); // Assert Assert.IsNotNull(trajectory, "轨迹不应该为null"); Assert.IsTrue(trajectory.ActualRadius > 0, "实际半径应该大于0"); Assert.IsTrue(Math.Abs(trajectory.ArcCenter.Z) > 0.01, "圆心Z坐标应该不为0"); } #endregion #region SampleArc 测试 [TestMethod] public void Standalone_SampleArc_WithValidTrajectory_ReturnsSampledPoints() { // Arrange var trajectory = new TestArcTrajectory { Ts = new TestPoint3D(0, 2, 0), Te = new TestPoint3D(2, 0, 0), ArcCenter = new TestPoint3D(0, 0, 0), ActualRadius = 2.0, DeflectionAngle = Math.PI / 2, // 90度 ArcLength = Math.PI // 半圆周长 }; double samplingStep = 0.5; // Act var sampledPoints = TestPathCurveEngine.SampleArc(trajectory, samplingStep); // Assert Assert.IsNotNull(sampledPoints, "采样点列表不应该为null"); Assert.IsTrue(sampledPoints.Count >= 2, "采样点数量应该至少为2"); Assert.AreEqual(trajectory.Ts, sampledPoints.First(), "第一个点应该是进入切点"); Assert.AreEqual(trajectory.Te, sampledPoints.Last(), "最后一个点应该是退出切点"); } [TestMethod] public void Standalone_SampleArc_SmallArcLength_ReturnsThreePoints() { // Arrange var trajectory = new TestArcTrajectory { Ts = new TestPoint3D(0, 0.1, 0), Te = new TestPoint3D(0.1, 0, 0), ArcCenter = new TestPoint3D(0, 0, 0), ActualRadius = 0.1, DeflectionAngle = 0.1, ArcLength = 0.01 // 非常小的圆弧 }; double samplingStep = 0.05; // Act var sampledPoints = TestPathCurveEngine.SampleArc(trajectory, samplingStep); // Assert Assert.IsNotNull(sampledPoints, "采样点列表不应该为null"); Assert.AreEqual(3, sampledPoints.Count, "小圆弧应该返回3个点(起点、中间点、终点)"); } [TestMethod] public void Standalone_SampleArc_SamplingStep0_05_ReturnsCorrectCount() { // Arrange var trajectory = new TestArcTrajectory { Ts = new TestPoint3D(0, 2, 0), Te = new TestPoint3D(2, 0, 0), ArcCenter = new TestPoint3D(0, 0, 0), ActualRadius = 2.0, DeflectionAngle = Math.PI / 2, ArcLength = Math.PI }; double samplingStep = 0.05; // Act var sampledPoints = TestPathCurveEngine.SampleArc(trajectory, samplingStep); // Assert int expectedCount = (int)Math.Ceiling(Math.PI / 0.05) + 1; Assert.IsTrue(sampledPoints.Count >= expectedCount - 2 && sampledPoints.Count <= expectedCount + 2, $"采样点数量应该在 {expectedCount - 2} 到 {expectedCount + 2} 之间"); } #endregion #region 集成测试 [TestMethod] public void FullWorkflow_RightAngleTurn_CalculatesCorrectTrajectory() { // Arrange - 创建一个90度转弯 var pPrev = new TestPoint3D(0, 10, 0); var pCurr = new TestPoint3D(0, 0, 0); var pNext = new TestPoint3D(10, 0, 0); double turnRadius = 2.0; // Act - 计算轨迹 var trajectory = TestPathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); // 采样圆弧 var sampledPoints = TestPathCurveEngine.SampleArc(trajectory, 0.5); // Assert Assert.IsNotNull(trajectory, "轨迹不应该为null"); Assert.IsTrue(trajectory.ActualRadius > 0, "实际半径应该大于0"); // 验证圆弧长度 Assert.IsTrue(trajectory.ArcLength > 0 && trajectory.ArcLength < 10, "圆弧长度应该合理"); // 验证采样点 Assert.IsNotNull(sampledPoints, "采样点不应该为null"); Assert.IsTrue(sampledPoints.Count > 2, "采样点数量应该大于2"); // 验证所有采样点到圆心的距离应该接近半径 foreach (var point in sampledPoints) { double dist = (point - trajectory.ArcCenter).Length; Assert.IsTrue(Math.Abs(dist - trajectory.ActualRadius) < 0.1, $"采样点到圆心的距离应该接近半径: {dist} vs {trajectory.ActualRadius}"); } } [TestMethod] public void FullWorkflow_AcuteAngleTurn_CalculatesCorrectTrajectory() { // Arrange - 创建一个60度锐角转弯 var pPrev = new TestPoint3D(0, 10, 0); var pCurr = new TestPoint3D(0, 0, 0); var pNext = new TestPoint3D(8.66, 5, 0); double turnRadius = 2.0; // Act var trajectory = TestPathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); var sampledPoints = TestPathCurveEngine.SampleArc(trajectory, 0.5); // Assert Assert.IsNotNull(trajectory, "轨迹不应该为null"); Assert.IsTrue(trajectory.DeflectionAngle > 0.5 && trajectory.DeflectionAngle < 1.5, "偏转角应该在60度左右"); Assert.IsTrue(sampledPoints.Count > 2, "采样点数量应该大于2"); } #endregion } }