From 93135d3c2916b76ed783f3765d884c00c95b608f Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Tue, 30 Dec 2025 18:29:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E6=9B=B2=E7=BA=BF=E5=8C=96=E7=9A=84=E6=96=B9=E6=A1=88=EF=BC=8C?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BA=86=E5=9F=BA=E7=A1=80=E7=9A=84=E6=9B=B2?= =?UTF-8?q?=E7=BA=BF=E5=8C=96=E8=B7=AF=E5=BE=84=E6=95=B0=E6=8D=AE=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=92=8C=E5=AD=98=E5=82=A8=EF=BC=8C=E4=BB=A5=E5=8F=8A?= =?UTF-8?q?=E6=9B=B2=E7=BA=BF=E5=8C=96=E6=A0=B8=E5=BF=83=E7=AE=97=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NavisworksTransport.UnitTests.csproj | 53 +- NavisworksTransportPlugin.csproj | 1 + ...hreadSafeObservableCollectionBasicTests.cs | 2 + .../Core/PathCurveEngineStandaloneTests.cs | 565 ++++++++++++++++ UnitTests/Core/PathCurveEngineTests.cs | 518 +++++++++++++++ UnitTests/SimpleTest.cs | 36 + UnitTests/TestHelpers/TestViewModel.cs | 3 + config.toml.example | 8 + doc/working/路径曲线化实施方案_20251230.md | 614 ++++++++++++++++++ run-unit-tests.bat | 2 +- src/Core/Config/SystemConfig.cs | 14 + src/Core/PathCurveEngine.cs | 354 ++++++++++ src/Core/PathDataManager.cs | 63 ++ src/Core/PathDatabase.cs | 214 +++++- src/Core/PathPlanningManager.cs | 12 + src/Core/PathPlanningModels.cs | 142 +++- 16 files changed, 2559 insertions(+), 42 deletions(-) create mode 100644 UnitTests/Core/PathCurveEngineStandaloneTests.cs create mode 100644 UnitTests/Core/PathCurveEngineTests.cs create mode 100644 UnitTests/SimpleTest.cs create mode 100644 doc/working/路径曲线化实施方案_20251230.md create mode 100644 src/Core/PathCurveEngine.cs diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj index 219f778..39d5249 100644 --- a/NavisworksTransport.UnitTests.csproj +++ b/NavisworksTransport.UnitTests.csproj @@ -16,27 +16,34 @@ true - + true full false - bin\Debug\ + bin\x64\Debug\ DEBUG;TRACE prompt 4 x64 - + pdbonly true - bin\Release\ + bin\x64\Release\ TRACE prompt 4 + x64 + + + ..\..\..\..\Program Files\Autodesk\Navisworks Manage 2026\Autodesk.Navisworks.Api.dll + False + + @@ -48,6 +55,7 @@ + @@ -67,45 +75,28 @@ - + bin\x64\Debug\NavisworksTransportPlugin.dll True - - - - packages\RoyT.AStar.3.0.2\lib\netstandard2.0\Roy-T.AStar.dll - True - - - - - packages\geometry4Sharp.1.0.0\lib\net462\geometry4Sharp.dll - True - - - - - - - - - - - - - - + + + + + - + + + + \ No newline at end of file diff --git a/NavisworksTransportPlugin.csproj b/NavisworksTransportPlugin.csproj index 4ef64b7..7bcd54f 100644 --- a/NavisworksTransportPlugin.csproj +++ b/NavisworksTransportPlugin.csproj @@ -112,6 +112,7 @@ + diff --git a/UnitTests/Collections/ThreadSafeObservableCollectionBasicTests.cs b/UnitTests/Collections/ThreadSafeObservableCollectionBasicTests.cs index 6f2343b..f580fdd 100644 --- a/UnitTests/Collections/ThreadSafeObservableCollectionBasicTests.cs +++ b/UnitTests/Collections/ThreadSafeObservableCollectionBasicTests.cs @@ -1,3 +1,5 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + namespace NavisworksTransport.UnitTests { /// diff --git a/UnitTests/Core/PathCurveEngineStandaloneTests.cs b/UnitTests/Core/PathCurveEngineStandaloneTests.cs new file mode 100644 index 0000000..df0219a --- /dev/null +++ b/UnitTests/Core/PathCurveEngineStandaloneTests.cs @@ -0,0 +1,565 @@ +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 + } +} \ No newline at end of file diff --git a/UnitTests/Core/PathCurveEngineTests.cs b/UnitTests/Core/PathCurveEngineTests.cs new file mode 100644 index 0000000..f7e7544 --- /dev/null +++ b/UnitTests/Core/PathCurveEngineTests.cs @@ -0,0 +1,518 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Autodesk.Navisworks.Api; + +namespace NavisworksTransport.UnitTests.Core +{ + /// + /// PathCurveEngine 核心算法测试 + /// 测试路径曲线化算法的正确性 + /// + [TestClass] + public class PathCurveEngineTests + { + #region CalculateFillet 测试 + + [TestMethod] + public void CalculateFillet_RightAngleTurn_ReturnsValidArc() + { + // Arrange - 创建90度直角转弯 + var pPrev = new Point3D(0, 10, 0); // 上方点 + var pCurr = new Point3D(0, 0, 0); // 转弯点 + var pNext = new Point3D(10, 0, 0); // 右方点 + double turnRadius = 2.0; + + // Act + var trajectory = PathCurveEngine.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(trajectory.Ts.X == 0, "进入切点X坐标应该为0"); + Assert.IsTrue(trajectory.Te.X > 0, "退出切点应该在转弯点右侧"); + Assert.IsTrue(trajectory.Te.Y == 0, "退出切点Y坐标应该为0"); + } + + [TestMethod] + public void CalculateFillet_AcuteAngleTurn_ReturnsValidArc() + { + // Arrange - 创建锐角转弯(约60度) + var pPrev = new Point3D(0, 10, 0); + var pCurr = new Point3D(0, 0, 0); + var pNext = new Point3D(8.66, 5, 0); // 60度方向 + double turnRadius = 2.0; + + // Act + var trajectory = PathCurveEngine.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 CalculateFillet_ObtuseAngleTurn_ReturnsValidArc() + { + // Arrange - 创建钝角转弯(约120度) + var pPrev = new Point3D(0, 10, 0); + var pCurr = new Point3D(0, 0, 0); + var pNext = new Point3D(8.66, -5, 0); // 120度方向 + double turnRadius = 2.0; + + // Act + var trajectory = PathCurveEngine.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 CalculateFillet_CollinearPoints_ReturnsInvalidArc() + { + // Arrange - 创建共线点(几乎直线) + var pPrev = new Point3D(0, 10, 0); + var pCurr = new Point3D(0, 0, 0); + var pNext = new Point3D(0, -10, 0); // 同一直线 + double turnRadius = 2.0; + + // Act + var trajectory = PathCurveEngine.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 CalculateFillet_SafetyTruncation_AdjustsRadius() + { + // Arrange - 创建短边场景,需要安全截断 + var pPrev = new Point3D(0, 1, 0); // 短边 + var pCurr = new Point3D(0, 0, 0); + var pNext = new Point3D(1, 0, 0); // 短边 + double turnRadius = 2.0; // 半径大于边长 + + // Act + var trajectory = PathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); + + // Assert + Assert.IsNotNull(trajectory, "轨迹不应该为null"); + Assert.IsTrue(trajectory.ActualRadius < turnRadius, "实际半径应该小于请求半径"); + Assert.IsTrue(trajectory.ActualRadius > 0, "实际半径应该大于0"); + } + + [TestMethod] + public void CalculateFillet_3DTurn_ReturnsValidArc() + { + // Arrange - 创建3D空间中的转弯 + var pPrev = new Point3D(0, 10, 0); + var pCurr = new Point3D(0, 0, 0); + var pNext = new Point3D(10, 0, 5); // 有Z轴变化 + double turnRadius = 2.0; + + // Act + var trajectory = PathCurveEngine.CalculateFillet(pPrev, pCurr, pNext, turnRadius); + + // Assert + Assert.IsNotNull(trajectory, "轨迹不应该为null"); + Assert.IsTrue(trajectory.ActualRadius > 0, "实际半径应该大于0"); + Assert.IsTrue(trajectory.ArcCenter.Z != 0, "圆心Z坐标应该不为0"); + } + + #endregion + + #region SampleArc 测试 + + [TestMethod] + public void SampleArc_WithValidTrajectory_ReturnsSampledPoints() + { + // Arrange + var trajectory = new ArcTrajectory + { + Ts = new Point3D(0, 2, 0), + Te = new Point3D(2, 0, 0), + ArcCenter = new Point3D(0, 0, 0), + ActualRadius = 2.0, + DeflectionAngle = Math.PI / 2, // 90度 + ArcLength = Math.PI // 半圆周长 + }; + double samplingStep = 0.5; + + // Act + var sampledPoints = PathCurveEngine.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 SampleArc_SmallArcLength_ReturnsTwoPoints() + { + // Arrange + var trajectory = new ArcTrajectory + { + Ts = new Point3D(0, 0.1, 0), + Te = new Point3D(0.1, 0, 0), + ArcCenter = new Point3D(0, 0, 0), + ActualRadius = 0.1, + DeflectionAngle = 0.1, + ArcLength = 0.01 // 非常小的圆弧 + }; + double samplingStep = 0.05; + + // Act + var sampledPoints = PathCurveEngine.SampleArc(trajectory, samplingStep); + + // Assert + Assert.IsNotNull(sampledPoints, "采样点列表不应该为null"); + Assert.AreEqual(2, sampledPoints.Count, "小圆弧应该返回2个点"); + } + + [TestMethod] + public void SampleArc_SamplingStep0_05_ReturnsCorrectCount() + { + // Arrange + var trajectory = new ArcTrajectory + { + Ts = new Point3D(0, 2, 0), + Te = new Point3D(2, 0, 0), + ArcCenter = new Point3D(0, 0, 0), + ActualRadius = 2.0, + DeflectionAngle = Math.PI / 2, + ArcLength = Math.PI + }; + double samplingStep = 0.05; + + // Act + var sampledPoints = PathCurveEngine.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 ApplyCurvatureToRoute 测试 + + [TestMethod] + public void ApplyCurvatureToRoute_SimplePath_GeneratesEdges() + { + // Arrange + var route = new PathRoute + { + Id = Guid.NewGuid().ToString(), + Name = "测试路径", + TurnRadius = 2.0 + }; + route.Points = new List + { + new PathPoint(new Point3D(0, 0, 0), "起点", PathPointType.StartPoint), + new PathPoint(new Point3D(10, 0, 0), "点2", PathPointType.WayPoint), + new PathPoint(new Point3D(10, 10, 0), "终点", PathPointType.EndPoint) + }; + route.Points[0].Index = 0; + route.Points[1].Index = 1; + route.Points[2].Index = 2; + double samplingStep = 0.5; + + // Act + PathCurveEngine.ApplyCurvatureToRoute(route, samplingStep); + + // Assert + Assert.IsTrue(route.IsCurved, "路径应该被标记为已曲线化"); + Assert.IsTrue(route.Edges.Count > 0, "应该生成路径边"); + Assert.IsTrue(route.TotalLength > 0, "路径总长度应该大于0"); + + // 验证每个边都有采样点 + foreach (var edge in route.Edges) + { + Assert.IsNotNull(edge.SampledPoints, "每个边都应该有采样点"); + Assert.IsTrue(edge.SampledPoints.Count > 0, "采样点数量应该大于0"); + } + } + + [TestMethod] + public void ApplyCurvatureToRoute_LShapedPath_GeneratesArcEdge() + { + // Arrange + var route = new PathRoute + { + Id = Guid.NewGuid().ToString(), + Name = "L型路径", + TurnRadius = 1.5 + }; + route.Points = new List + { + new PathPoint(new Point3D(0, 10, 0), "起点", PathPointType.StartPoint), + new PathPoint(new Point3D(0, 0, 0), "转弯点", PathPointType.WayPoint), + new PathPoint(new Point3D(10, 0, 0), "终点", PathPointType.EndPoint) + }; + route.Points[0].Index = 0; + route.Points[1].Index = 1; + route.Points[2].Index = 2; + double samplingStep = 0.5; + + // Act + PathCurveEngine.ApplyCurvatureToRoute(route, samplingStep); + + // Assert + Assert.IsTrue(route.IsCurved, "路径应该被标记为已曲线化"); + Assert.AreEqual(2, route.Edges.Count, "L型路径应该生成2条边"); + + // 第一条边是直线段 + Assert.AreEqual(PathSegmentType.Straight, route.Edges[0].SegmentType, "第一条边应该是直线段"); + + // 第二条边包含圆弧 + Assert.AreEqual(PathSegmentType.Arc, route.Edges[1].SegmentType, "第二条边应该是圆弧段"); + Assert.IsNotNull(route.Edges[1].Trajectory, "圆弧边应该有轨迹数据"); + Assert.IsTrue(route.Edges[1].Trajectory.ActualRadius > 0, "实际半径应该大于0"); + } + + [TestMethod] + public void ApplyCurvatureToRoute_ZShapedPath_GeneratesMultipleArcEdges() + { + // Arrange + var route = new PathRoute + { + Id = Guid.NewGuid().ToString(), + Name = "Z型路径", + TurnRadius = 1.5 + }; + route.Points = new List + { + new PathPoint(new Point3D(0, 10, 0), "起点", PathPointType.StartPoint), + new PathPoint(new Point3D(0, 0, 0), "转弯点1", PathPointType.WayPoint), + new PathPoint(new Point3D(10, 0, 0), "转弯点2", PathPointType.WayPoint), + new PathPoint(new Point3D(10, 10, 0), "终点", PathPointType.EndPoint) + }; + route.Points[0].Index = 0; + route.Points[1].Index = 1; + route.Points[2].Index = 2; + route.Points[3].Index = 3; + double samplingStep = 0.5; + + // Act + PathCurveEngine.ApplyCurvatureToRoute(route, samplingStep); + + // Assert + Assert.IsTrue(route.IsCurved, "路径应该被标记为已曲线化"); + Assert.AreEqual(3, route.Edges.Count, "Z型路径应该生成3条边"); + + // 应该有两条圆弧边 + int arcEdgeCount = route.Edges.Count(e => e.SegmentType == PathSegmentType.Arc); + Assert.IsTrue(arcEdgeCount >= 1, "应该至少有一条圆弧边"); + } + + [TestMethod] + public void ApplyCurvatureToRoute_NullRoute_DoesNotThrow() + { + // Arrange + PathRoute route = null; + double samplingStep = 0.5; + + // Act & Assert + try + { + PathCurveEngine.ApplyCurvatureToRoute(route, samplingStep); + Assert.Fail("应该抛出ArgumentNullException"); + } + catch (ArgumentNullException) + { + // Expected + } + } + + [TestMethod] + public void ApplyCurvatureToRoute_LessThanTwoPoints_DoesNotCurve() + { + // Arrange + var route = new PathRoute + { + Id = Guid.NewGuid().ToString(), + Name = "单点路径", + TurnRadius = 2.0 + }; + route.Points = new List + { + new PathPoint(new Point3D(0, 0, 0), "起点", PathPointType.StartPoint) + }; + route.Points[0].Index = 0; + double samplingStep = 0.5; + + // Act + PathCurveEngine.ApplyCurvatureToRoute(route, samplingStep); + + // Assert + Assert.IsFalse(route.IsCurved, "少于2个点的路径不应该被曲线化"); + Assert.AreEqual(0, route.Edges.Count, "不应该生成任何边"); + } + + #endregion + + #region RecalculateRouteLength 测试 + + [TestMethod] + public void RecalculateRouteLength_StraightEdges_CalculatesCorrectly() + { + // Arrange + var route = new PathRoute + { + Id = Guid.NewGuid().ToString(), + Name = "测试路径" + }; + route.Edges = new List + { + new PathEdge + { + Id = Guid.NewGuid().ToString(), + SegmentType = PathSegmentType.Straight, + PhysicalLength = 10.0, + SampledPoints = new List() + }, + new PathEdge + { + Id = Guid.NewGuid().ToString(), + SegmentType = PathSegmentType.Straight, + PhysicalLength = 5.0, + SampledPoints = new List() + } + }; + + // Act + PathCurveEngine.RecalculateRouteLength(route); + + // Assert + Assert.AreEqual(15.0, route.TotalLength, 0.01, "路径总长度应该等于所有边长度之和"); + } + + [TestMethod] + public void RecalculateRouteLength_MixedEdgeTypes_CalculatesCorrectly() + { + // Arrange + var route = new PathRoute + { + Id = Guid.NewGuid().ToString(), + Name = "测试路径" + }; + route.Edges = new List + { + new PathEdge + { + Id = Guid.NewGuid().ToString(), + SegmentType = PathSegmentType.Straight, + PhysicalLength = 10.0, + SampledPoints = new List() + }, + new PathEdge + { + Id = Guid.NewGuid().ToString(), + SegmentType = PathSegmentType.Arc, + PhysicalLength = Math.PI, // 半圆 + SampledPoints = new List() + }, + new PathEdge + { + Id = Guid.NewGuid().ToString(), + SegmentType = PathSegmentType.Straight, + PhysicalLength = 5.0, + SampledPoints = new List() + } + }; + + // Act + PathCurveEngine.RecalculateRouteLength(route); + + // Assert + double expectedLength = 10.0 + Math.PI + 5.0; + Assert.AreEqual(expectedLength, route.TotalLength, 0.01, "路径总长度应该正确计算"); + } + + [TestMethod] + public void RecalculateRouteLength_NullRoute_DoesNotThrow() + { + // Arrange + PathRoute route = null; + + // Act + PathCurveEngine.RecalculateRouteLength(route); + + // Assert - 不应该抛出异常 + Assert.IsTrue(true, "null路径不应该导致异常"); + } + + [TestMethod] + public void RecalculateRouteLength_EmptyEdges_SetsZeroLength() + { + // Arrange + var route = new PathRoute + { + Id = Guid.NewGuid().ToString(), + Name = "空路径", + Edges = new List() + }; + + // Act + PathCurveEngine.RecalculateRouteLength(route); + + // Assert + Assert.AreEqual(0, route.TotalLength, 0.01, "空边的路径长度应该为0"); + } + + #endregion + + #region 集成测试 + + [TestMethod] + public void FullWorkflow_CreateCurvePath_CalculatesCorrectLength() + { + // Arrange - 创建一个包含转弯的路径 + var route = new PathRoute + { + Id = Guid.NewGuid().ToString(), + Name = "完整测试路径", + TurnRadius = 2.0 + }; + route.Points = new List + { + new PathPoint(new Point3D(0, 20, 0), "起点", PathPointType.StartPoint), + new PathPoint(new Point3D(0, 0, 0), "转弯点", PathPointType.WayPoint), + new PathPoint(new Point3D(20, 0, 0), "终点", PathPointType.EndPoint) + }; + route.Points[0].Index = 0; + route.Points[1].Index = 1; + route.Points[2].Index = 2; + double samplingStep = 0.5; + + // Act - 应用曲线化 + PathCurveEngine.ApplyCurvatureToRoute(route, samplingStep); + + // Assert + Assert.IsTrue(route.IsCurved, "路径应该被曲线化"); + Assert.IsTrue(route.TotalLength > 0, "路径总长度应该大于0"); + + // 验证路径总长度小于直线距离(因为圆弧比直线短) + double straightDistance = 20 + 20; // 两条直线段 + Assert.IsTrue(route.TotalLength < straightDistance, "曲线化后的路径长度应该小于直线距离"); + } + + #endregion + } +} \ No newline at end of file diff --git a/UnitTests/SimpleTest.cs b/UnitTests/SimpleTest.cs new file mode 100644 index 0000000..9b86f00 --- /dev/null +++ b/UnitTests/SimpleTest.cs @@ -0,0 +1,36 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace NavisworksTransport.UnitTests +{ + [TestClass] + public class SimpleTest + { + [TestMethod] + public void HelloWorld_ShouldPass() + { + // Arrange + string expected = "Hello World"; + + // Act + string actual = "Hello World"; + + // Assert + Assert.AreEqual(expected, actual, "Hello World 测试应该通过"); + } + + [TestMethod] + public void SimpleMath_ShouldReturnCorrectSum() + { + // Arrange + int a = 2; + int b = 3; + int expected = 5; + + // Act + int actual = a + b; + + // Assert + Assert.AreEqual(expected, actual, "2 + 3 应该等于 5"); + } + } +} \ No newline at end of file diff --git a/UnitTests/TestHelpers/TestViewModel.cs b/UnitTests/TestHelpers/TestViewModel.cs index 2da2dc8..c2f6042 100644 --- a/UnitTests/TestHelpers/TestViewModel.cs +++ b/UnitTests/TestHelpers/TestViewModel.cs @@ -1,3 +1,6 @@ +using System; +using System.Threading.Tasks; + namespace NavisworksTransport.UnitTests { /// diff --git a/config.toml.example b/config.toml.example index 3538812..21e157e 100644 --- a/config.toml.example +++ b/config.toml.example @@ -20,6 +20,14 @@ vehicle_height_meters = 2.0 # 安全间隙(米) safety_margin_meters = 0.05 +# 路径曲线化配置 + +# 路径默认转弯半径(米)- 表示路径允许的最大转弯半径 +default_path_turn_radius = 2.5 + +# 圆弧采样步长(米)- 推荐值:0.02-0.1 +arc_sampling_step = 0.05 + [visualization] # 地图边距比例(0-1之间) margin_ratio = 0.1 diff --git a/doc/working/路径曲线化实施方案_20251230.md b/doc/working/路径曲线化实施方案_20251230.md new file mode 100644 index 0000000..e9beb26 --- /dev/null +++ b/doc/working/路径曲线化实施方案_20251230.md @@ -0,0 +1,614 @@ +无人物流车转弯路径曲线化功能 - 实施方案 + +1. 目标概述 +实现基于圆弧过渡(Arc Fillet)的路径曲线化功能,替代现有的直线连接方式,确保仿真系统能准确检测车辆转弯时的扫掠路径(Swept Path)碰撞。 + +核心原理 +控制点与物理点分离:用户操作的 +PathPoint + 作为控制点,实际物理路径由 PathEdge 表示 +圆弧过渡法:在相邻路径段转折处插入切圆弧,使用进入切点 Ts 和退出切点 Te 连接 +安全截断:当计算的切线长度超过线段长度45%时,自动缩减半径,防止圆弧过大 +2. 数据结构改动 +2.1 新增数据模型 +[NEW] PathSegmentType 枚举 +/// +/// 路径段类型 +/// +public enum PathSegmentType +{ + /// + /// 直线段 + /// + Straight, + + /// + /// 圆弧段 + /// + Arc +} +[NEW] ArcTrajectory 类 +/// +/// 圆弧轨迹数据 +/// +[Serializable] +public class ArcTrajectory +{ + /// + /// 进入切点 + /// + public Point3D Ts { get; set; } + + /// + /// 退出切点 + /// + public Point3D Te { get; set; } + + /// + /// 圆心位置 + /// + public Point3D ArcCenter { get; set; } + + /// + /// 请求半径(配置的转向半径) + /// + public double RequestedRadius { get; set; } + + /// + /// 实际半径(安全截断后) + /// + public double ActualRadius { get; set; } + + /// + /// 偏转角(弧度) + /// + public double DeflectionAngle { get; set; } + + /// + /// 圆弧长度(米) + /// + public double ArcLength { get; set; } +} +[NEW] PathEdge 类 +/// +/// 路径边 - 连接两个连续控制点的物理路径段 +/// +[Serializable] +public class PathEdge +{ + /// + /// 边唯一标识符 + /// + public string Id { get; set; } + + /// + /// 起始控制点ID + /// + public string StartPointId { get; set; } + + /// + /// 结束控制点ID + /// + public string EndPointId { get; set; } + + /// + /// 路径段类型 + /// + public PathSegmentType SegmentType { get; set; } + + /// + /// 圆弧轨迹数据(仅当 SegmentType == Arc 时有效) + /// + public ArcTrajectory Trajectory { get; set; } + + /// + /// 边的物理长度(米) + /// 直线段:两点间距离;圆弧段:直线段长度 + 圆弧长度 + /// + public double PhysicalLength { get; set; } + + /// + /// 采样点序列(用于碰撞检测和动画) + /// + [XmlIgnore] + public List SampledPoints { get; set; } +} +2.2 修改现有数据模型 +[MODIFY] +PathPoint.cs +新增属性: + +/// +/// 自定义转向半径(米),用于局部急转弯场景 +/// 为 null 时使用全局默认值 +/// +public double? CustomTurnRadius { get; set; } +[MODIFY] +PathRoute.cs +新增属性: + +/// +/// 路径边集合 - 存储物理路径段 +/// +public List Edges { get; set; } = new List(); +/// +/// 默认转向半径(米) +/// +public double TurnRadius { get; set; } = 1.5; +/// +/// 是否已曲线化 +/// +public bool IsCurved { get; set; } = false; +修改方法: + +UpdateTotalLength() +: 综合计算所有 Edges 的物理长度 +Clone() +: 需要深拷贝 Edges 集合 +2.3 数据库表结构扩展 +[MODIFY] +PathDatabase.cs +新增表 PathEdges: + +CREATE TABLE IF NOT EXISTS PathEdges ( + Id TEXT PRIMARY KEY, + RouteId TEXT NOT NULL, + StartPointId TEXT NOT NULL, + EndPointId TEXT NOT NULL, + SegmentType INTEGER NOT NULL, -- 0=Straight, 1=Arc + PhysicalLength REAL NOT NULL, + + -- 圆弧轨迹数据 (仅 SegmentType=1 时有效) + Ts_X REAL, + Ts_Y REAL, + Ts_Z REAL, + Te_X REAL, + Te_Y REAL, + Te_Z REAL, + ArcCenter_X REAL, + ArcCenter_Y REAL, + ArcCenter_Z REAL, + RequestedRadius REAL, + ActualRadius REAL, + DeflectionAngle REAL, + ArcLength REAL, + + FOREIGN KEY (RouteId) REFERENCES PathRoutes(Id) ON DELETE CASCADE +); +修改表 +PathRoutes +: + +ALTER TABLE PathRoutes ADD COLUMN TurnRadius REAL DEFAULT 1.5; +ALTER TABLE PathRoutes ADD COLUMN IsCurved INTEGER DEFAULT 0; +修改表 +PathPoints +: + +ALTER TABLE PathPoints ADD COLUMN CustomTurnRadius REAL; +3. 核心算法实现 +3.1 PathCurveEngine 类 +位置: src/Core/PathCurveEngine.cs (新建) + +3.1.1 CalculateFillet 方法 +/// +/// 计算圆弧切点和轨迹参数 +/// +/// 前一个控制点 +/// 当前控制点 +/// 下一个控制点 +/// 转向半径(米) +/// 圆弧轨迹数据 +public static ArcTrajectory CalculateFillet( + Point3D pPrev, + Point3D pCurr, + Point3D pNext, + double turnRadius) +{ + // 1. 计算单位向量 + Vector3D v1 = (pPrev - pCurr).Normalize(); + Vector3D v2 = (pNext - pCurr).Normalize(); + + // 2. 计算夹角 α + double cosAlpha = Vector3D.DotProduct(v1, v2); + double angleRad = Math.Acos(Math.Clamp(cosAlpha, -1.0, 1.0)); + + // 3. 计算切线长 Lt = R / tan(α/2) + double Lt = turnRadius / Math.Tan(angleRad / 2.0); + + // 4. 安全截断检查 + 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. 计算切点 + Point3D ts = pCurr + v1 * Lt; + Point3D te = pCurr + v2 * Lt; + + // 6. 计算圆心 (使用角平分线方向) + Vector3D bisector = (v1 + v2).Normalize(); + double distToCenter = actualRadius / Math.Sin(angleRad / 2.0); + Point3D arcCenter = pCurr + bisector * distToCenter; + + // 7. 计算圆弧长度 + double arcLength = actualRadius * angleRad; + + return new ArcTrajectory + { + Ts = ts, + Te = te, + ArcCenter = arcCenter, + RequestedRadius = turnRadius, + ActualRadius = actualRadius, + DeflectionAngle = angleRad, + ArcLength = arcLength + }; +} +3.1.2 SampleArc 方法 +/// +/// 圆弧采样为离散点序列 +/// +/// 圆弧轨迹 +/// 采样步长(米) +/// 采样点列表 +public static List SampleArc( + ArcTrajectory trajectory, + double samplingStep) +{ + var points = new List(); + + // 根据采样步长计算采样点数量 + int sampleCount = Math.Max(2, (int)Math.Ceiling(trajectory.ArcLength / samplingStep)); + + // 计算起始和结束向量 + Vector3D startVec = (trajectory.Ts - trajectory.ArcCenter).Normalize(); + Vector3D endVec = (trajectory.Te - trajectory.ArcCenter).Normalize(); + + // 计算旋转轴(叉乘) + Vector3D rotationAxis = Vector3D.CrossProduct(startVec, endVec).Normalize(); + + // 等角度插值 + for (int i = 0; i <= sampleCount; i++) + { + double t = i / (double)sampleCount; + double theta = t * trajectory.DeflectionAngle; + + // Rodrigues旋转公式 + Point3D sampledPoint = RotatePointAroundAxis( + trajectory.Ts, + trajectory.ArcCenter, + rotationAxis, + theta + ); + + points.Add(sampledPoint); + } + + return points; +} +3.1.3 ApplyCurvatureToRoute 方法 +/// +/// 对路径应用曲线化处理 +/// +/// 待处理的路径 +/// 采样步长(米) +public static void ApplyCurvatureToRoute(PathRoute route, double samplingStep) +{ + route.Edges.Clear(); + var sortedPoints = route.GetSortedPoints(); + + if (sortedPoints.Count < 2) + { + route.IsCurved = false; + return; + } + + for (int i = 0; i < sortedPoints.Count - 1; i++) + { + var p1 = sortedPoints[i]; + var p2 = sortedPoints[i + 1]; + + // 判断是否为转折点(需要圆弧过渡) + bool needsArc = (i > 0 && i < sortedPoints.Count - 2); + + if (needsArc) + { + var p0 = sortedPoints[i - 1]; + var p2Next = sortedPoints[i + 2]; + + // 获取转向半径(优先使用自定义值) + double radius = p1.CustomTurnRadius ?? route.TurnRadius; + + // 计算圆弧 + var arcTraj = CalculateFillet(p0, p1, p2, radius); + + // 创建Edge(包含直线段 + 圆弧段) + var edge = BuildEdgeWithArc(p1, p2, arcTraj, samplingStep); + route.Edges.Add(edge); + } + else + { + // 直线边 + var edge = BuildStraightEdge(p1, p2, samplingStep); + route.Edges.Add(edge); + } + } + + route.IsCurved = true; + route.RecalculateLength(); +} +4. 配置文件扩展 +[MODIFY] +SystemConfig.cs +在 +PathEditingConfig + 类中新增: + +/// +/// 默认转向半径(米) +/// 推荐值:1.0-2.0 +/// +public double DefaultTurnRadius { get; set; } = 1.5; +/// +/// 圆弧采样步长(米) +/// 推荐值:0.02-0.1 +/// +public double ArcSamplingStep { get; set; } = 0.05; + +配置文件示例 (TOML) +[PathEditing] +CellSizeMeters = 0.5 +MaxHeightDiffMeters = 0.35 +VehicleLengthMeters = 1.0 +VehicleWidthMeters = 1.0 +VehicleHeightMeters = 2.0 +SafetyMarginMeters = 0.05 + +# 路径曲线化配置 + +DefaultTurnRadius = 1.5 +ArcSamplingStep = 0.05 + +1. 业务流程集成 +5.1 路径编辑完成流程 +[MODIFY] +PathPlanningManager.cs + +- + +FinishEditing + 方法 +public bool FinishEditing() +{ + try + { + if (_currentRoute == null) + return false; + + // **新增:应用曲线化** + if (ConfigManager.Instance.Current.PathEditing.EnablePathCurving) + { + double samplingStep = ConfigManager.Instance.Current.PathEditing.ArcSamplingStep; + _currentRoute.TurnRadius = ConfigManager.Instance.Current.PathEditing.DefaultTurnRadius; + + PathCurveEngine.ApplyCurvatureToRoute(_currentRoute, samplingStep); + LogManager.Info($"路径曲线化完成: {_currentRoute.Name}, 边数: {_currentRoute.Edges.Count}"); + } + + // 保存到数据库(包含Edges) + SaveRouteToDatabase(_currentRoute); + + // ... 现有逻辑 ... + } + catch (Exception ex) + { + LogManager.Error($"完成编辑失败: {ex.Message}"); + return false; + } +} +5.2 数据持久化 +[MODIFY] +PathDatabase.cs +新增方法: + +SavePathEdges(string routeId, List edges) +LoadPathEdges(string routeId) : List +DeletePathEdges(string routeId) +修改方法: + +SavePathRoute(PathRoute route) +: 同时保存 Edges +GetAllPathRoutes() +: 同时加载 Edges +DeletePathRoute(string routeId) +: 级联删除 Edges +5.3 导出格式扩展 +[MODIFY] +PathDataManager.cs +DELMIA XML 格式: + + + + + + + + + 1.5 + + + + +CSV 格式(扁平化采样点): + +RouteId,EdgeIndex,SegmentType,PointIndex,X,Y,Z +route_001,0,Arc,0,10.5,5.2,0.0 +route_001,0,Arc,1,10.52,5.25,0.0 +... +6. 碰撞检测与动画集成 +6.1 碰撞检测 +[MODIFY] PathAnimationManager.cs - 碰撞检测逻辑 +// 从 Edges 获取采样点而非直接使用 Points +foreach (var edge in route.Edges) +{ + if (edge.SampledPoints == null || edge.SampledPoints.Count == 0) + { + // 懒加载:若采样点未生成,现场生成 + edge.SampledPoints = GenerateSampledPoints(edge, arcSamplingStep); + } + + foreach (var point in edge.SampledPoints) + { + // 执行OBB碰撞检测 ... + } +} +6.2 动画生成 +使用 edge.SampledPoints 代替原始控制点生成动画帧序列。 + +1. 验证计划 +7.1 单元测试 +新建测试文件: test/Core/PathCurveEngineTests.cs + +测试用例: + +基础几何计算 + +验证90度直角转弯的圆弧计算 +验证锐角/钝角场景 +验证3D空间旋转正确性 +安全截断 + +验证半径过大时自动缩减 +边界条件:最小夹角(~0°)和最大夹角(~180°) +采样精度 + +验证采样点数量符合步长要求 +验证采样点均匀分布在圆弧上 +运行命令(需确认实际测试框架): + +# 假设使用 xUnit + +dotnet test NavisworksTransport.Tests --filter "FullyQualifiedName~PathCurveEngine" +7.2 集成测试 +场景测试路径: + +创建包含3个控制点的简单L型路径 +设置转向半径 = 1.5m +完成编辑触发曲线化 +验证: +Edges 数量= 2 +第一个Edge为直线段,第二个Edge包含圆弧 +TotalLength 正确(直线段 + 圆弧段) +重新加载路径验证持久化 +7.3 手动验证 +IMPORTANT + +以下步骤需要用户在Navisworks中手动验证 + +测试步骤: + +打开Navisworks,加载测试模型 +进入路径规划模式,手工添加4个控制点形成Z字形路径 +在配置文件中设置: +DefaultTurnRadius = 2.0 +ArcSamplingStep = 0.05 +EnablePathCurving = true +完成编辑,观察路径可视化是否显示平滑曲线(而非折线) +运行碰撞检测,检查转弯处的采样点密度 +导出DELMIA XML,用文本编辑器验证圆弧轨迹参数存在 +预期结果: + +路径在转折点处显示圆弧过渡 +碰撞检测能捕获内轮差区域的碰撞 +XML包含完整的 节点 +8. 风险与注意事项 +8.1 性能影响 +圆弧采样:每个转折点生成 ArcLength / SamplingStep 个采样点 +缓解措施:默认步长0.05m平衡精度与性能,支持配置调整 +8.2 数据兼容性 +旧版路径数据库无 Edges 表 +迁移策略: +检测表是否存在,不存在则创建 +加载旧路径时,Edges为空视为"未曲线化",用户完成编辑时自动曲线化 +8.3 极端场景 +非常小的转向半径 + 锐角:可能导致无法满足几何约束 +处理:最小半径限制0.3m,警告日志记录 +9. 实施顺序建议 +✅ 数据结构 → 新增类和修改现有模型 +✅ 配置管理 → 扩展SystemConfig和TOML +✅ 核心算法 → PathCurveEngine + 单元测试 +✅ 数据库 → 表结构变更 + CRUD方法 +✅ 业务集成 → FinishEditing + 数据持久化 +✅ 导出扩展 → XML/CSV格式支持 +✅ 碰撞检测 → 使用Edges采样点 +✅ 验证测试 → 集成测试 + 手动验证 +10. 后续优化方向 +支持Clothoid曲线(回旋曲线)以实现更平滑的加速度变化 +UI可视化:实时预览圆弧位置 +性能优化:采样点缓存机制 + +物流车转弯路径曲线化功能实施任务规划 +总体目标 +实现基于圆弧过渡(Arc Fillet)的路径曲线化功能,替代现有的直线连接方式,满足仿真系统对物理真实性的要求。 + +实施阶段 +[ ] 阶段1: 数据结构设计与扩展 + 设计 Edge(边)数据结构 + PathEdge 类定义 + PathSegmentType 枚举(直线/圆弧) + ArcTrajectory 圆弧轨迹数据 + 扩展 PathPoint 支持局部半径覆盖 + 新增 CustomTurnRadius 属性 + 重构 PathRoute 类 + 新增 Edges 集合 + 新增 TurnRadius 全局转向半径 + 更新长度计算逻辑 + 更新数据库表结构 + 设计 Edges 表结构 + 设计数据迁移策略 +[ ] 阶段2: 曲线化算法实现 + 创建 PathCurveEngine 核心算法类 + CalculateFillet 方法(圆弧切点计算) + 安全截断逻辑实现 + SampleArc 方法(圆弧采样) + 实现路径曲线化主流程 + ApplyCurvatureToRoute 方法 + BuildEdgesFromPoints 方法 + 单元测试 + 基础几何计算测试 + 边界条件测试 +[ ] 阶段3: 配置管理 + 添加曲线化相关配置项 + DefaultTurnRadius (默认转向半径) + ArcSamplingStep (圆弧采样步长,默认0.05m) + EnablePathCurving (启用开关) + 更新配置文件读取/保存逻辑 +[ ] 阶段4: 集成到现有业务流程 + 修改 PathRoute 长度计算 + 直线段 + 圆弧段综合计算 + 集成到路径编辑完成流程 + FinishEditing 触发曲线化 + 更新数据持久化 + PathDatabase 保存/加载 Edges + PathDataManager 导出格式扩展 +[ ] 阶段5: 碰撞检测与动画集成 + 碰撞检测使用曲线路径 + 采样点序列生成 + 集成到现有碰撞检测 + PathAnimationManager 支持曲线路径 + 使用 Edges 生成动画帧 +[ ] 阶段6: DELMIA 导出扩展 + XML Tag Group 格式支持圆弧 + 导出 Ts, Te, ArcCenter + CSV 格式导出采样点序列 +[ ] 阶段7: 测试与验证 + 功能测试 + 性能测试 + 边界场景测试 diff --git a/run-unit-tests.bat b/run-unit-tests.bat index c0fb828..04dc3f9 100644 --- a/run-unit-tests.bat +++ b/run-unit-tests.bat @@ -29,7 +29,7 @@ if not exist "%MSBUILD_PATH%" ( echo Using MSBuild: "%MSBUILD_PATH%" echo Building test project... -"%MSBUILD_PATH%" NavisworksTransport.UnitTests.csproj /p:Configuration=Debug /p:Platform=AnyCPU /verbosity:minimal +"%MSBUILD_PATH%" NavisworksTransport.UnitTests.csproj /p:Configuration=Release /p:Platform=x64 /verbosity:minimal if errorlevel 1 ( echo Test project build failed! diff --git a/src/Core/Config/SystemConfig.cs b/src/Core/Config/SystemConfig.cs index f54a484..e607d9e 100644 --- a/src/Core/Config/SystemConfig.cs +++ b/src/Core/Config/SystemConfig.cs @@ -87,6 +87,20 @@ namespace NavisworksTransport.Core.Config /// 安全间隙(米) /// public double SafetyMarginMeters { get; set; } = 0.05; + + // 路径曲线化配置 + + /// + /// 路径默认转弯半径(米) + /// 表示路径允许的最大转弯半径,车辆的最小转弯半径必须小于等于此值 + /// + public double DefaultPathTurnRadius { get; set; } = 2.5; + + /// + /// 圆弧采样步长(米) + /// 推荐值:0.02-0.1 + /// + public double ArcSamplingStep { get; set; } = 0.05; } /// diff --git a/src/Core/PathCurveEngine.cs b/src/Core/PathCurveEngine.cs new file mode 100644 index 0000000..3eacc68 --- /dev/null +++ b/src/Core/PathCurveEngine.cs @@ -0,0 +1,354 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Autodesk.Navisworks.Api; + +namespace NavisworksTransport +{ + /// + /// 路径曲线化引擎 + /// 实现基于圆弧过渡(Arc Fillet)的路径曲线化功能 + /// + public static class PathCurveEngine + { + /// + /// 将值限制在指定范围内(.NET Framework 4.8 不支持 Math.Clamp) + /// + private static double Clamp(double value, double min, double max) + { + if (value < min) return min; + if (value > max) return max; + return value; + } + + /// + /// 计算圆弧切点和轨迹参数 + /// + /// 前一个控制点 + /// 当前控制点 + /// 下一个控制点 + /// 转向半径(米) + /// 圆弧轨迹数据 + public static ArcTrajectory CalculateFillet( + Point3D pPrev, + Point3D pCurr, + Point3D pNext, + double turnRadius) + { + // 1. 计算单位向量 + Vector3D v1 = (pPrev - pCurr).Normalize(); + Vector3D v2 = (pNext - pCurr).Normalize(); + + // 2. 计算夹角 α + double cosAlpha = GeometryHelper.DotProduct( + new Point3D(v1.X, v1.Y, v1.Z), + new Point3D(v2.X, v2.Y, v2.Z)); + double angleRad = Math.Acos(Clamp(cosAlpha, -1.0, 1.0)); + + // 如果几乎共线,返回无效轨迹 + if (angleRad < 0.01 || angleRad > Math.PI - 0.01) + { + return new ArcTrajectory + { + 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. 安全截断检查 + 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. 计算切点 + Point3D ts = pCurr + v1 * Lt; + Point3D te = pCurr + v2 * Lt; + + // 6. 计算圆心 (使用角平分线方向) + Vector3D bisector = (v1 + v2).Normalize(); + double distToCenter = actualRadius / Math.Sin(angleRad / 2.0); + Point3D arcCenter = pCurr + bisector * distToCenter; + + // 7. 计算圆弧长度 + double arcLength = actualRadius * angleRad; + + return new ArcTrajectory + { + Ts = ts, + Te = te, + ArcCenter = arcCenter, + RequestedRadius = turnRadius, + ActualRadius = actualRadius, + DeflectionAngle = angleRad, + ArcLength = arcLength + }; + } + + /// + /// Rodrigues旋转公式 - 绕任意轴旋转点 + /// + /// 要旋转的点 + /// 旋转中心 + /// 旋转轴(单位向量) + /// 旋转角度(弧度) + /// 旋转后的点 + private static Point3D RotatePointAroundAxis( + Point3D point, + Point3D center, + Vector3D axis, + double angle) + { + // 将点转换为相对于中心的向量 + Vector3D v = point - center; + + // Rodrigues旋转公式 + // v_rot = v * cos(θ) + (k × v) * sin(θ) + k * (k · v) * (1 - cos(θ)) + Point3D kxv = GeometryHelper.CrossProduct( + new Point3D(axis.X, axis.Y, axis.Z), + new Point3D(v.X, v.Y, v.Z)); + double kdv = GeometryHelper.DotProduct( + new Point3D(axis.X, axis.Y, axis.Z), + new Point3D(v.X, v.Y, v.Z)); + + Vector3D kxvVec = new Vector3D(kxv.X, kxv.Y, kxv.Z); + Vector3D vRot = v * Math.Cos(angle) + kxvVec * Math.Sin(angle) + axis * kdv * (1 - Math.Cos(angle)); + + return center + vRot; + } + + /// + /// 圆弧采样为离散点序列 + /// + /// 圆弧轨迹 + /// 采样步长(米) + /// 采样点列表 + public static List SampleArc( + ArcTrajectory trajectory, + double samplingStep) + { + var points = new List(); + + // 如果圆弧长度很小,直接返回起止点 + if (trajectory.ArcLength < 0.001) + { + points.Add(trajectory.Ts); + points.Add(trajectory.Te); + return points; + } + + // 根据采样步长计算采样点数量 + int sampleCount = Math.Max(2, (int)Math.Ceiling(trajectory.ArcLength / samplingStep)); + + // 计算起始和结束向量 + Vector3D startVec = (trajectory.Ts - trajectory.ArcCenter).Normalize(); + Vector3D endVec = (trajectory.Te - trajectory.ArcCenter).Normalize(); + + // 计算旋转轴(叉乘) + Point3D cross = GeometryHelper.CrossProduct( + new Point3D(startVec.X, startVec.Y, startVec.Z), + new Point3D(endVec.X, endVec.Y, endVec.Z)); + Vector3D rotationAxis = new Vector3D(cross.X, cross.Y, cross.Z).Normalize(); + + // 等角度插值 + for (int i = 0; i <= sampleCount; i++) + { + double t = i / (double)sampleCount; + double theta = t * trajectory.DeflectionAngle; + + // Rodrigues旋转公式 + Point3D sampledPoint = RotatePointAroundAxis( + trajectory.Ts, + trajectory.ArcCenter, + rotationAxis, + theta + ); + + points.Add(sampledPoint); + } + + return points; + } + + /// + /// 构建直线边 + /// + /// 起始点 + /// 结束点 + /// 采样步长(米) + /// 路径边 + private static PathEdge BuildStraightEdge( + PathPoint p1, + PathPoint p2, + double samplingStep) + { + var edge = new PathEdge + { + StartPointId = p1.Id, + EndPointId = p2.Id, + SegmentType = PathSegmentType.Straight, + PhysicalLength = (p1.Position - p2.Position).Length + }; + + // 采样直线段 + int sampleCount = Math.Max(2, (int)Math.Ceiling(edge.PhysicalLength / samplingStep)); + edge.SampledPoints = new List(); + + for (int i = 0; i <= sampleCount; i++) + { + double t = i / (double)sampleCount; + Point3D sampledPoint = p1.Position + (p2.Position - p1.Position) * t; + edge.SampledPoints.Add(sampledPoint); + } + + return edge; + } + + /// + /// 构建包含圆弧的边 + /// + /// 起始点 + /// 结束点 + /// 圆弧轨迹 + /// 采样步长(米) + /// 路径边 + private static PathEdge BuildEdgeWithArc( + PathPoint p1, + PathPoint p2, + ArcTrajectory arcTrajectory, + double samplingStep) + { + var edge = new PathEdge + { + StartPointId = p1.Id, + EndPointId = p2.Id, + SegmentType = PathSegmentType.Arc, + Trajectory = arcTrajectory, + PhysicalLength = arcTrajectory.ArcLength + }; + + // 采样圆弧段 + edge.SampledPoints = SampleArc(arcTrajectory, samplingStep); + + return edge; + } + + /// + /// 生成采样点序列(用于延迟加载) + /// + /// 路径边 + /// 采样步长(米) + /// 采样点列表 + public static List GenerateSampledPoints(PathEdge edge, double samplingStep) + { + if (edge.SegmentType == PathSegmentType.Straight) + { + // 直线段采样 + int sampleCount = Math.Max(2, (int)Math.Ceiling(edge.PhysicalLength / samplingStep)); + var points = new List(); + + for (int i = 0; i <= sampleCount; i++) + { + double t = i / (double)sampleCount; + Point3D p1 = edge.Trajectory?.Ts ?? edge.Trajectory?.Te ?? new Point3D(); + Point3D p2 = edge.Trajectory?.Te ?? new Point3D(); + Point3D sampledPoint = p1 + (p2 - p1) * t; + points.Add(sampledPoint); + } + + return points; + } + else if (edge.SegmentType == PathSegmentType.Arc && edge.Trajectory != null) + { + // 圆弧段采样 + return SampleArc(edge.Trajectory, samplingStep); + } + + return new List(); + } + + /// + /// 对路径应用曲线化处理 + /// + /// 待处理的路径 + /// 采样步长(米) + public static void ApplyCurvatureToRoute(PathRoute route, double samplingStep) + { + if (route == null) + throw new ArgumentNullException(nameof(route)); + + route.Edges.Clear(); + var sortedPoints = route.Points.OrderBy(p => p.Index).ToList(); + + if (sortedPoints.Count < 2) + { + route.IsCurved = false; + return; + } + + // 处理每一段路径 + for (int i = 0; i < sortedPoints.Count - 1; i++) + { + var p1 = sortedPoints[i]; + var p2 = sortedPoints[i + 1]; + + // 判断是否为转折点(需要圆弧过渡) + bool needsArc = (i > 0 && i < sortedPoints.Count - 1); + + if (needsArc) + { + var p0 = sortedPoints[i - 1]; + var p2Next = sortedPoints[i + 2]; + + // 获取转向半径(优先使用自定义值) + double radius = p1.CustomTurnRadius ?? route.TurnRadius; + + // 计算圆弧 + var arcTraj = CalculateFillet(p0.Position, p1.Position, p2Next.Position, radius); + + // 创建Edge(包含圆弧段) + var edge = BuildEdgeWithArc(p1, p2, arcTraj, samplingStep); + route.Edges.Add(edge); + } + else + { + // 直线边 + var edge = BuildStraightEdge(p1, p2, samplingStep); + route.Edges.Add(edge); + } + } + + route.IsCurved = true; + RecalculateRouteLength(route); + } + + /// + /// 重新计算路径总长度 + /// + /// 路径 + public static void RecalculateRouteLength(PathRoute route) + { + if (route == null || route.Edges == null) + { + route.TotalLength = 0; + return; + } + + route.TotalLength = route.Edges.Sum(e => e.PhysicalLength); + } + } +} \ No newline at end of file diff --git a/src/Core/PathDataManager.cs b/src/Core/PathDataManager.cs index 224463d..3d5f624 100644 --- a/src/Core/PathDataManager.cs +++ b/src/Core/PathDataManager.cs @@ -959,6 +959,69 @@ namespace NavisworksTransport pointsElement.AppendChild(pointElement); } + // 添加路径边 + if (route.Edges != null && route.Edges.Count > 0) + { + var edgesElement = xmlDoc.CreateElement("Edges", _delmiaNamespace); + routeElement.AppendChild(edgesElement); + + foreach (var edge in route.Edges) + { + var edgeElement = xmlDoc.CreateElement("Edge", _delmiaNamespace); + edgeElement.SetAttribute("id", edge.Id); + edgeElement.SetAttribute("type", edge.SegmentType.ToString().ToLower()); + edgeElement.SetAttribute("startPointId", edge.StartPointId); + edgeElement.SetAttribute("endPointId", edge.EndPointId); + edgeElement.SetAttribute("physicalLength", edge.PhysicalLength.ToString("F6")); + + // 圆弧轨迹数据 + if (edge.SegmentType == PathSegmentType.Arc && edge.Trajectory != null) + { + var trajElement = xmlDoc.CreateElement("Trajectory", _delmiaNamespace); + + // 进入切点 + var tsElement = xmlDoc.CreateElement("Ts", _delmiaNamespace); + tsElement.SetAttribute("x", edge.Trajectory.Ts.X.ToString("F3")); + tsElement.SetAttribute("y", edge.Trajectory.Ts.Y.ToString("F3")); + tsElement.SetAttribute("z", edge.Trajectory.Ts.Z.ToString("F3")); + trajElement.AppendChild(tsElement); + + // 退出切点 + var teElement = xmlDoc.CreateElement("Te", _delmiaNamespace); + teElement.SetAttribute("x", edge.Trajectory.Te.X.ToString("F3")); + teElement.SetAttribute("y", edge.Trajectory.Te.Y.ToString("F3")); + teElement.SetAttribute("z", edge.Trajectory.Te.Z.ToString("F3")); + trajElement.AppendChild(teElement); + + // 圆心 + var centerElement = xmlDoc.CreateElement("ArcCenter", _delmiaNamespace); + centerElement.SetAttribute("x", edge.Trajectory.ArcCenter.X.ToString("F3")); + centerElement.SetAttribute("y", edge.Trajectory.ArcCenter.Y.ToString("F3")); + centerElement.SetAttribute("z", edge.Trajectory.ArcCenter.Z.ToString("F3")); + trajElement.AppendChild(centerElement); + + // 半径 + var radiusElement = xmlDoc.CreateElement("Radius", _delmiaNamespace); + radiusElement.InnerText = edge.Trajectory.ActualRadius.ToString("F3"); + trajElement.AppendChild(radiusElement); + + // 偏转角 + var angleElement = xmlDoc.CreateElement("DeflectionAngle", _delmiaNamespace); + angleElement.InnerText = edge.Trajectory.DeflectionAngle.ToString("F6"); + trajElement.AppendChild(angleElement); + + // 圆弧长度 + var arcLengthElement = xmlDoc.CreateElement("ArcLength", _delmiaNamespace); + arcLengthElement.InnerText = edge.Trajectory.ArcLength.ToString("F3"); + trajElement.AppendChild(arcLengthElement); + + edgeElement.AppendChild(trajElement); + } + + edgesElement.AppendChild(edgeElement); + } + } + return routeElement; } diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs index a311b0e..35bdfac 100644 --- a/src/Core/PathDatabase.cs +++ b/src/Core/PathDatabase.cs @@ -64,6 +64,8 @@ namespace NavisworksTransport Name TEXT NOT NULL, TotalLength REAL, EstimatedTime REAL, + TurnRadius REAL DEFAULT 0.0, -- 实际使用时从配置文件获取默认值 + IsCurved INTEGER DEFAULT 0, MaxVehicleLength REAL, MaxVehicleWidth REAL, MaxVehicleHeight REAL, @@ -105,7 +107,7 @@ namespace NavisworksTransport ) "); - // 4. 路径点表 + // 4. 路径点表(新增 CustomTurnRadius) ExecuteNonQuery(@" CREATE TABLE IF NOT EXISTS PathPoints ( Id TEXT PRIMARY KEY, @@ -116,6 +118,33 @@ namespace NavisworksTransport Y REAL, Z REAL, Type INTEGER, + CustomTurnRadius REAL, + FOREIGN KEY(RouteId) REFERENCES PathRoutes(Id) ON DELETE CASCADE + ) + "); + + // 5. 路径边表(新增) + ExecuteNonQuery(@" + CREATE TABLE IF NOT EXISTS PathEdges ( + Id TEXT PRIMARY KEY, + RouteId TEXT NOT NULL, + StartPointId TEXT NOT NULL, + EndPointId TEXT NOT NULL, + SegmentType INTEGER NOT NULL, + PhysicalLength REAL NOT NULL, + Ts_X REAL, + Ts_Y REAL, + Ts_Z REAL, + Te_X REAL, + Te_Y REAL, + Te_Z REAL, + ArcCenter_X REAL, + ArcCenter_Y REAL, + ArcCenter_Z REAL, + RequestedRadius REAL, + ActualRadius REAL, + DeflectionAngle REAL, + ArcLength REAL, FOREIGN KEY(RouteId) REFERENCES PathRoutes(Id) ON DELETE CASCADE ) "); @@ -123,6 +152,7 @@ namespace NavisworksTransport // 创建索引 ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_reports_route ON CollisionReports(RouteId)"); ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_pathpoints_route ON PathPoints(RouteId)"); + ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_pathedges_route ON PathEdges(RouteId)"); } /// @@ -138,8 +168,8 @@ namespace NavisworksTransport // 保存路径基本信息 var sql = @" INSERT OR REPLACE INTO PathRoutes - (Id, Name, TotalLength, EstimatedTime, MaxVehicleLength, MaxVehicleWidth, MaxVehicleHeight, SafetyMargin, GridSize, CreatedTime, LastModified) - VALUES (@id, @name, @length, @time, @maxLength, @maxWidth, @maxHeight, @safetyMargin, @gridSize, @created, @modified) + (Id, Name, TotalLength, EstimatedTime, TurnRadius, IsCurved, MaxVehicleLength, MaxVehicleWidth, MaxVehicleHeight, SafetyMargin, GridSize, CreatedTime, LastModified) + VALUES (@id, @name, @length, @time, @turnRadius, @isCurved, @maxLength, @maxWidth, @maxHeight, @safetyMargin, @gridSize, @created, @modified) "; using (var cmd = new SQLiteCommand(sql, _connection)) @@ -148,6 +178,8 @@ namespace NavisworksTransport cmd.Parameters.AddWithValue("@name", route.Name); cmd.Parameters.AddWithValue("@length", route.TotalLength); cmd.Parameters.AddWithValue("@time", route.EstimatedTime); + cmd.Parameters.AddWithValue("@turnRadius", route.TurnRadius); + cmd.Parameters.AddWithValue("@isCurved", route.IsCurved ? 1 : 0); cmd.Parameters.AddWithValue("@maxLength", route.MaxVehicleLength); cmd.Parameters.AddWithValue("@maxWidth", route.MaxVehicleWidth); cmd.Parameters.AddWithValue("@maxHeight", route.MaxVehicleHeight); @@ -158,16 +190,17 @@ namespace NavisworksTransport cmd.ExecuteNonQuery(); } - // 删除旧的路径点 - ExecuteNonQuery("DELETE FROM PathPoints WHERE RouteId=@id", new { id = route.Id }); + // 删除旧的路径点和边 + DeletePathPoints(route.Id); + DeletePathEdges(route.Id); // 保存新的路径点 if (route.Points != null && route.Points.Count > 0) { var pointSql = @" INSERT INTO PathPoints - (Id, RouteId, SequenceNumber, Name, X, Y, Z, Type) - VALUES (@id, @routeId, @seq, @name, @x, @y, @z, @type) + (Id, RouteId, SequenceNumber, Name, X, Y, Z, Type, CustomTurnRadius) + VALUES (@id, @routeId, @seq, @name, @x, @y, @z, @type, @customRadius) "; for (int i = 0; i < route.Points.Count; i++) @@ -183,6 +216,8 @@ namespace NavisworksTransport cmd.Parameters.AddWithValue("@y", point.Position.Y); cmd.Parameters.AddWithValue("@z", point.Position.Z); cmd.Parameters.AddWithValue("@type", (int)point.Type); + cmd.Parameters.AddWithValue("@customRadius", point.CustomTurnRadius.HasValue ? + (object)point.CustomTurnRadius.Value : DBNull.Value); cmd.ExecuteNonQuery(); } } @@ -190,6 +225,17 @@ namespace NavisworksTransport LogManager.Info($"保存路径: {route.Name},包含 {route.Points.Count} 个路径点"); } + // 保存路径边 + if (route.Edges != null && route.Edges.Count > 0) + { + foreach (var edge in route.Edges) + { + SavePathEdge(route.Id, edge); + } + + LogManager.Info($"保存路径: {route.Name},包含 {route.Edges.Count} 个路径边"); + } + transaction.Commit(); } catch (Exception ex) @@ -441,6 +487,8 @@ namespace NavisworksTransport Name = reader["Name"].ToString(), TotalLength = Convert.ToDouble(reader["TotalLength"]), EstimatedTime = Convert.ToDouble(reader["EstimatedTime"]), + TurnRadius = reader.IsDBNull(reader.GetOrdinal("TurnRadius")) ? 0.0 : Convert.ToDouble(reader["TurnRadius"]), // 0.0 表示未设置,实际使用时从配置获取 + IsCurved = reader.IsDBNull(reader.GetOrdinal("IsCurved")) ? false : Convert.ToInt32(reader["IsCurved"]) == 1, MaxVehicleLength = reader.IsDBNull(reader.GetOrdinal("MaxVehicleLength")) ? 1.0 : Convert.ToDouble(reader["MaxVehicleLength"]), MaxVehicleWidth = reader.IsDBNull(reader.GetOrdinal("MaxVehicleWidth")) ? 1.0 : Convert.ToDouble(reader["MaxVehicleWidth"]), MaxVehicleHeight = reader.IsDBNull(reader.GetOrdinal("MaxVehicleHeight")) ? 2.0 : Convert.ToDouble(reader["MaxVehicleHeight"]), @@ -463,10 +511,11 @@ namespace NavisworksTransport } } - // 为每个路径加载路径点 + // 为每个路径加载路径点和路径边 foreach (var route in routes) { LoadPathPoints(route); + LoadPathEdges(route); } LogManager.Info($"从数据库加载了 {routes.Count} 条路径记录"); @@ -501,12 +550,161 @@ namespace NavisworksTransport Type = (PathPointType)Convert.ToInt32(reader["Type"]) }; + // 加载自定义转向半径 + if (!reader.IsDBNull(reader.GetOrdinal("CustomTurnRadius"))) + { + point.CustomTurnRadius = Convert.ToDouble(reader["CustomTurnRadius"]); + } + route.Points.Add(point); } } } } + /// + /// 保存路径边 + /// + private void SavePathEdge(string routeId, PathEdge edge) + { + var sql = @" + INSERT INTO PathEdges + (Id, RouteId, StartPointId, EndPointId, SegmentType, PhysicalLength, + Ts_X, Ts_Y, Ts_Z, Te_X, Te_Y, Te_Z, + ArcCenter_X, ArcCenter_Y, ArcCenter_Z, + RequestedRadius, ActualRadius, DeflectionAngle, ArcLength) + VALUES (@id, @routeId, @startId, @endId, @segType, @length, + @tsx, @tsy, @tsz, @tex, @tey, @tez, + @acx, @acy, @acz, + @reqR, @actR, @angle, @arcLen) + "; + + using (var cmd = new SQLiteCommand(sql, _connection)) + { + cmd.Parameters.AddWithValue("@id", edge.Id); + cmd.Parameters.AddWithValue("@routeId", routeId); + cmd.Parameters.AddWithValue("@startId", edge.StartPointId); + cmd.Parameters.AddWithValue("@endId", edge.EndPointId); + cmd.Parameters.AddWithValue("@segType", (int)edge.SegmentType); + cmd.Parameters.AddWithValue("@length", edge.PhysicalLength); + + // 圆弧轨迹数据 + if (edge.SegmentType == PathSegmentType.Arc && edge.Trajectory != null) + { + cmd.Parameters.AddWithValue("@tsx", edge.Trajectory.Ts.X); + cmd.Parameters.AddWithValue("@tsy", edge.Trajectory.Ts.Y); + cmd.Parameters.AddWithValue("@tsz", edge.Trajectory.Ts.Z); + cmd.Parameters.AddWithValue("@tex", edge.Trajectory.Te.X); + cmd.Parameters.AddWithValue("@tey", edge.Trajectory.Te.Y); + cmd.Parameters.AddWithValue("@tez", edge.Trajectory.Te.Z); + cmd.Parameters.AddWithValue("@acx", edge.Trajectory.ArcCenter.X); + cmd.Parameters.AddWithValue("@acy", edge.Trajectory.ArcCenter.Y); + cmd.Parameters.AddWithValue("@acz", edge.Trajectory.ArcCenter.Z); + cmd.Parameters.AddWithValue("@reqR", edge.Trajectory.RequestedRadius); + cmd.Parameters.AddWithValue("@actR", edge.Trajectory.ActualRadius); + cmd.Parameters.AddWithValue("@angle", edge.Trajectory.DeflectionAngle); + cmd.Parameters.AddWithValue("@arcLen", edge.Trajectory.ArcLength); + } + else + { + // 直线段,圆弧参数为NULL + cmd.Parameters.AddWithValue("@tsx", DBNull.Value); + cmd.Parameters.AddWithValue("@tsy", DBNull.Value); + cmd.Parameters.AddWithValue("@tsz", DBNull.Value); + cmd.Parameters.AddWithValue("@tex", DBNull.Value); + cmd.Parameters.AddWithValue("@tey", DBNull.Value); + cmd.Parameters.AddWithValue("@tez", DBNull.Value); + cmd.Parameters.AddWithValue("@acx", DBNull.Value); + cmd.Parameters.AddWithValue("@acy", DBNull.Value); + cmd.Parameters.AddWithValue("@acz", DBNull.Value); + cmd.Parameters.AddWithValue("@reqR", DBNull.Value); + cmd.Parameters.AddWithValue("@actR", DBNull.Value); + cmd.Parameters.AddWithValue("@angle", DBNull.Value); + cmd.Parameters.AddWithValue("@arcLen", DBNull.Value); + } + + cmd.ExecuteNonQuery(); + } + } + + /// + /// 删除路径点 + /// + private void DeletePathPoints(string routeId) + { + using (var cmd = new SQLiteCommand("DELETE FROM PathPoints WHERE RouteId = @routeId", _connection)) + { + cmd.Parameters.AddWithValue("@routeId", routeId); + cmd.ExecuteNonQuery(); + } + } + + /// + /// 删除路径边 + /// + private void DeletePathEdges(string routeId) + { + using (var cmd = new SQLiteCommand("DELETE FROM PathEdges WHERE RouteId = @routeId", _connection)) + { + cmd.Parameters.AddWithValue("@routeId", routeId); + cmd.ExecuteNonQuery(); + } + } + + /// + /// 加载路径边 + /// + private void LoadPathEdges(PathRoute route) + { + var sql = "SELECT * FROM PathEdges WHERE RouteId = @routeId ORDER BY Id"; + + using (var cmd = new SQLiteCommand(sql, _connection)) + { + cmd.Parameters.AddWithValue("@routeId", route.Id); + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var edge = new PathEdge + { + Id = reader["Id"].ToString(), + StartPointId = reader["StartPointId"].ToString(), + EndPointId = reader["EndPointId"].ToString(), + SegmentType = (PathSegmentType)Convert.ToInt32(reader["SegmentType"]), + PhysicalLength = Convert.ToDouble(reader["PhysicalLength"]) + }; + + // 加载圆弧轨迹数据 + if (edge.SegmentType == PathSegmentType.Arc) + { + edge.Trajectory = new ArcTrajectory + { + Ts = new Point3D( + Convert.ToDouble(reader["Ts_X"]), + Convert.ToDouble(reader["Ts_Y"]), + Convert.ToDouble(reader["Ts_Z"])), + Te = new Point3D( + Convert.ToDouble(reader["Te_X"]), + Convert.ToDouble(reader["Te_Y"]), + Convert.ToDouble(reader["Te_Z"])), + ArcCenter = new Point3D( + Convert.ToDouble(reader["ArcCenter_X"]), + Convert.ToDouble(reader["ArcCenter_Y"]), + Convert.ToDouble(reader["ArcCenter_Z"])), + RequestedRadius = Convert.ToDouble(reader["RequestedRadius"]), + ActualRadius = Convert.ToDouble(reader["ActualRadius"]), + DeflectionAngle = Convert.ToDouble(reader["DeflectionAngle"]), + ArcLength = Convert.ToDouble(reader["ArcLength"]) + }; + } + + route.Edges.Add(edge); + } + } + } + } + /// /// 执行非查询SQL语句 /// diff --git a/src/Core/PathPlanningManager.cs b/src/Core/PathPlanningManager.cs index b564fd0..db9ec7d 100644 --- a/src/Core/PathPlanningManager.cs +++ b/src/Core/PathPlanningManager.cs @@ -1466,6 +1466,18 @@ namespace NavisworksTransport } } + // === 应用曲线化 === + if (CurrentRoute != null) + { + double samplingStep = ConfigManager.Instance.Current.PathEditing.ArcSamplingStep; + + // 使用配置文件中的默认转弯半径 + CurrentRoute.TurnRadius = ConfigManager.Instance.Current.PathEditing.DefaultPathTurnRadius; + + PathCurveEngine.ApplyCurvatureToRoute(CurrentRoute, samplingStep); + LogManager.Info($"路径曲线化完成: {CurrentRoute.Name}, 转弯半径: {CurrentRoute.TurnRadius:F2}m, 边数: {CurrentRoute.Edges.Count}"); + } + // 如果是创建模式,将当前路径添加到路径集合 if (_pathEditState == PathEditState.Creating && CurrentRoute != null) { diff --git a/src/Core/PathPlanningModels.cs b/src/Core/PathPlanningModels.cs index 37e2c9f..d2b7cf4 100644 --- a/src/Core/PathPlanningModels.cs +++ b/src/Core/PathPlanningModels.cs @@ -269,6 +269,12 @@ namespace NavisworksTransport /// public double SpeedLimit { get; set; } + /// + /// 自定义转向半径(米),用于局部急转弯场景 + /// 为 null 时使用全局默认值 + /// + public double? CustomTurnRadius { get; set; } + /// /// 构造函数 /// @@ -282,6 +288,7 @@ namespace NavisworksTransport Index = 0; Notes = string.Empty; SpeedLimit = 0; + CustomTurnRadius = null; } /// @@ -300,6 +307,7 @@ namespace NavisworksTransport Index = 0; Notes = string.Empty; SpeedLimit = 0; + CustomTurnRadius = null; } /// @@ -312,6 +320,114 @@ namespace NavisworksTransport } } + /// + /// 路径段类型 + /// + public enum PathSegmentType + { + /// + /// 直线段 + /// + Straight, + + /// + /// 圆弧段 + /// + Arc + } + + /// + /// 圆弧轨迹数据 + /// + [Serializable] + public class ArcTrajectory + { + /// + /// 进入切点 + /// + public Point3D Ts { get; set; } + + /// + /// 退出切点 + /// + public Point3D Te { get; set; } + + /// + /// 圆心位置 + /// + public Point3D ArcCenter { get; set; } + + /// + /// 请求半径(配置的转向半径) + /// + public double RequestedRadius { get; set; } + + /// + /// 实际半径(安全截断后) + /// + public double ActualRadius { get; set; } + + /// + /// 偏转角(弧度) + /// + public double DeflectionAngle { get; set; } + + /// + /// 圆弧长度(米) + /// + public double ArcLength { get; set; } + } + + /// + /// 路径边 - 连接两个连续控制点的物理路径段 + /// + [Serializable] + public class PathEdge + { + /// + /// 边唯一标识符 + /// + public string Id { get; set; } + + /// + /// 起始控制点ID + /// + public string StartPointId { get; set; } + + /// + /// 结束控制点ID + /// + public string EndPointId { get; set; } + + /// + /// 路径段类型 + /// + public PathSegmentType SegmentType { get; set; } + + /// + /// 圆弧轨迹数据(仅当 SegmentType == Arc 时有效) + /// + public ArcTrajectory Trajectory { get; set; } + + /// + /// 边的物理长度(米) + /// 直线段:两点间距离;圆弧段:直线段长度 + 圆弧长度 + /// + public double PhysicalLength { get; set; } + + /// + /// 采样点序列(用于碰撞检测和动画) + /// + [XmlIgnore] + public List SampledPoints { get; set; } + + public PathEdge() + { + Id = Guid.NewGuid().ToString(); + SampledPoints = new List(); + } + } + /// /// 路径路线数据模型 /// @@ -319,10 +435,15 @@ namespace NavisworksTransport public class PathRoute { /// - /// 路径点集合 + /// 路径点集合(控制点) /// public List Points { get; set; } - + + /// + /// 路径边集合 - 存储物理路径段 + /// + public List Edges { get; set; } + /// /// 路径名称 /// @@ -414,6 +535,17 @@ namespace NavisworksTransport /// public double SafetyMargin { get; set; } = 0.5; + /// + /// 路径转弯半径(米)- 路径允许的最大转弯半径 + /// 车辆的最小转弯半径必须小于等于此值 + /// + public double TurnRadius { get; set; } = 0.0; + + /// + /// 是否已曲线化 + /// + public bool IsCurved { get; set; } = false; + // 数据库分析相关属性 /// /// 碰撞数量(从数据库加载) @@ -441,6 +573,7 @@ namespace NavisworksTransport public PathRoute() { Points = new List(); + Edges = new List(); Name = string.Empty; Id = Guid.NewGuid().ToString(); EstimatedTime = 0.0; @@ -449,6 +582,8 @@ namespace NavisworksTransport CreatedTime = DateTime.Now; LastModified = DateTime.Now; Description = string.Empty; + TurnRadius = 0.0; // 0.0 表示未设置,实际使用时从配置获取默认值 + IsCurved = false; } /// @@ -458,12 +593,15 @@ namespace NavisworksTransport public PathRoute(string name) { Points = new List(); + Edges = new List(); Name = name; Id = Guid.NewGuid().ToString(); EstimatedTime = 0.0; AssociatedChannelIds = new List(); TotalLength = 0.0; CreatedTime = DateTime.Now; + TurnRadius = 0.0; // 0.0 表示未设置,实际使用时从配置获取默认值 + IsCurved = false; LastModified = DateTime.Now; Description = string.Empty; }