diff --git a/UnitTests/CoordinateSystem/AssemblyEndFaceAnalyzerTests.cs b/UnitTests/CoordinateSystem/AssemblyEndFaceAnalyzerTests.cs index 4824dfc..401b846 100644 --- a/UnitTests/CoordinateSystem/AssemblyEndFaceAnalyzerTests.cs +++ b/UnitTests/CoordinateSystem/AssemblyEndFaceAnalyzerTests.cs @@ -63,6 +63,28 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem Assert.IsFalse(result.IsReliable); } + [TestMethod] + public void OrientNormalTowardTarget_ShouldFlip_WhenNormalOpposesTargetDirection() + { + Vector3 orientedNormal = AssemblyEndFaceAnalyzer.OrientNormalTowardTarget( + new Vector3(-1f, 0f, 0f), + Vector3.Zero, + new Vector3(10f, 0f, 0f)); + + AssertPoint(orientedNormal, 1.0, 0.0, 0.0); + } + + [TestMethod] + public void OrientNormalTowardTarget_ShouldKeepDirection_WhenNormalAlreadyFacesTarget() + { + Vector3 orientedNormal = AssemblyEndFaceAnalyzer.OrientNormalTowardTarget( + new Vector3(0f, 1f, 0f), + new Vector3(1f, 2f, 3f), + new Vector3(1f, 5f, 3f)); + + AssertPoint(orientedNormal, 0.0, 1.0, 0.0); + } + private static IEnumerable CreateRectangleFace(double minX, double maxX, double minY, double maxY, double z) { yield return new AnalysisTriangle3( diff --git a/UnitTests/CoordinateSystem/RailPathPoseHelperTests.cs b/UnitTests/CoordinateSystem/RailPathPoseHelperTests.cs new file mode 100644 index 0000000..cc93a59 --- /dev/null +++ b/UnitTests/CoordinateSystem/RailPathPoseHelperTests.cs @@ -0,0 +1,36 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NavisworksTransport.Utils; +using System.Numerics; + +namespace NavisworksTransport.UnitTests.CoordinateSystem +{ + [TestClass] + public class RailPathPoseHelperTests + { + [TestMethod] + public void NormalizePreferredNormalToHostUpHemisphere_ShouldFlip_WhenPreferredNormalOpposesHostUp() + { + Vector3 hostPreferredNormal = new Vector3(-0.1222f, -0.9676f, 0.2211f); + Vector3 normalized = RailPathPoseHelper.NormalizePreferredNormalToHostUpHemisphere( + hostPreferredNormal, + Vector3.UnitY); + + Assert.IsTrue(Vector3.Dot(normalized, Vector3.UnitY) > 0f); + Assert.AreEqual(0.1222 / 1.0008249, normalized.X, 1e-4); + Assert.AreEqual(0.9676 / 1.0008249, normalized.Y, 1e-4); + Assert.AreEqual(-0.2211 / 1.0008249, normalized.Z, 1e-4); + } + + [TestMethod] + public void NormalizePreferredNormalToHostUpHemisphere_ShouldKeepDirection_WhenPreferredNormalAlreadyMatchesHostUp() + { + Vector3 hostPreferredNormal = new Vector3(0.139f, 0.954f, 0.266f); + Vector3 normalized = RailPathPoseHelper.NormalizePreferredNormalToHostUpHemisphere( + hostPreferredNormal, + Vector3.UnitY); + + Assert.IsTrue(Vector3.Dot(normalized, Vector3.UnitY) > 0f); + Assert.IsTrue(Vector3.Dot(Vector3.Normalize(hostPreferredNormal), normalized) > 0.9999f); + } + } +} diff --git a/UnitTests/CoordinateSystem/ViewpointHelperTests.cs b/UnitTests/CoordinateSystem/ViewpointHelperTests.cs index 6858379..90dad79 100644 --- a/UnitTests/CoordinateSystem/ViewpointHelperTests.cs +++ b/UnitTests/CoordinateSystem/ViewpointHelperTests.cs @@ -21,8 +21,8 @@ namespace NavisworksTransport.UnitTests.CoordinateSystem Assert.AreEqual(8.0, hoisting.MinDistanceMeters, 1e-9); Assert.AreEqual(22.0, hoisting.ElevationDegrees, 1e-9); - Assert.AreEqual(0.9, rail.DistanceScale, 1e-9); - Assert.AreEqual(6.0, rail.MinDistanceMeters, 1e-9); + Assert.AreEqual(0.75, rail.DistanceScale, 1e-9); + Assert.AreEqual(4.5, rail.MinDistanceMeters, 1e-9); Assert.AreEqual(12.0, rail.ElevationDegrees, 1e-9); } diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index 5370876..6e01e40 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -1775,10 +1775,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels throw new InvalidOperationException("更新 Rail 路径起点失败。"); } - route.RailPreferredNormal = new Point3D( - _assemblyInstallationPlaneNormal.X, - _assemblyInstallationPlaneNormal.Y, - _assemblyInstallationPlaneNormal.Z); + route.RailPreferredNormal = CreatePersistedAssemblyPreferredNormal(); route.RailNormalOffset = UnitsConverter.ConvertFromMeters(AssemblyAnchorVerticalOffsetInMeters); route.LastModified = DateTime.Now; _assemblyInstallationReferenceRouteId = route.Id; @@ -2435,10 +2432,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels if (_hasAssemblyInstallationReference) { - route.RailPreferredNormal = new Point3D( - _assemblyInstallationPlaneNormal.X, - _assemblyInstallationPlaneNormal.Y, - _assemblyInstallationPlaneNormal.Z); + route.RailPreferredNormal = CreatePersistedAssemblyPreferredNormal(); } route.AddPoint(new PathPoint(startPoint, "起点", PathPointType.StartPoint)); @@ -2678,6 +2672,19 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } + private Point3D CreatePersistedAssemblyPreferredNormal() + { + HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); + Vector3 normalizedPreferredNormal = RailPathPoseHelper.NormalizePreferredNormalToHostUpHemisphere( + _assemblyInstallationPlaneNormal, + adapter.HostUpVector3); + + return new Point3D( + normalizedPreferredNormal.X, + normalizedPreferredNormal.Y, + normalizedPreferredNormal.Z); + } + private Vector3 GetCurrentAssemblyOpticalAxisDirection() { Point3D opticalAxisReferencePoint = GetAssemblyOpticalAxisReferencePoint(); @@ -3439,18 +3446,26 @@ namespace NavisworksTransport.UI.WPF.ViewModels } Point3D centerPoint = AssemblyEndFaceAnalyzer.ToPoint3D(result.Center); + Point3D sphereCenterPoint = new Point3D( + AssemblySphereCenterX, + AssemblySphereCenterY, + AssemblySphereCenterZ); + Vector3 orientedNormal = AssemblyEndFaceAnalyzer.OrientNormalTowardTarget( + result.Normal, + new Vector3((float)centerPoint.X, (float)centerPoint.Y, (float)centerPoint.Z), + new Vector3((float)sphereCenterPoint.X, (float)sphereCenterPoint.Y, (float)sphereCenterPoint.Z)); _hasAssemblyEndFaceAnalysis = true; _assemblyEndFaceCenterPoint = centerPoint; - _assemblyEndFaceNormal = result.Normal; + _assemblyEndFaceNormal = orientedNormal; RenderAssemblyEndFaceSeedPoints(); RenderAssemblyEndFaceCenter(centerPoint); - RenderAssemblyEndFaceNormal(centerPoint, result.Normal); + RenderAssemblyEndFaceNormal(centerPoint, orientedNormal); RefreshAssemblyTerminalObjectInfo(); RefreshAssemblyReferenceRodIfNeeded(); UpdateMainStatus($"端面分析完成:中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2}),候选三角形={result.CandidateTriangleCount}"); LogManager.Info( $"[直线装配] 端面分析完成: 中心=({centerPoint.X:F3}, {centerPoint.Y:F3}, {centerPoint.Z:F3}), " + - $"法向=({result.Normal.X:F4}, {result.Normal.Y:F4}, {result.Normal.Z:F4}), " + + $"法向=({orientedNormal.X:F4}, {orientedNormal.Y:F4}, {orientedNormal.Z:F4}), " + $"三角形={result.CandidateTriangleCount}, 顶点={result.CandidateVertexCount}, 偏差={result.MaxPlaneDeviation:F6}"); } diff --git a/src/Utils/Assembly/AssemblyEndFaceAnalyzer.cs b/src/Utils/Assembly/AssemblyEndFaceAnalyzer.cs index 7eda516..d77cb63 100644 --- a/src/Utils/Assembly/AssemblyEndFaceAnalyzer.cs +++ b/src/Utils/Assembly/AssemblyEndFaceAnalyzer.cs @@ -133,6 +133,29 @@ namespace NavisworksTransport.Utils.GeometryAnalysis return new Point3D(point.X, point.Y, point.Z); } + public static Vector3 OrientNormalTowardTarget(Vector3 normal, Vector3 originPoint, Vector3 targetPoint) + { + if (normal.LengthSquared() < 1e-12f) + { + throw new ArgumentException("端面法向长度过小,无法定向。", nameof(normal)); + } + + Vector3 normalizedNormal = Vector3.Normalize(normal); + Vector3 targetDirection = targetPoint - originPoint; + if (targetDirection.LengthSquared() < 1e-12f) + { + return normalizedNormal; + } + + Vector3 normalizedTargetDirection = Vector3.Normalize(targetDirection); + if (Vector3.Dot(normalizedNormal, normalizedTargetDirection) < 0f) + { + normalizedNormal = -normalizedNormal; + } + + return normalizedNormal; + } + private static AnalysisTriangle3 ConvertTriangle(Triangle3D triangle) { return new AnalysisTriangle3(ToVector3(triangle.Point1), ToVector3(triangle.Point2), ToVector3(triangle.Point3)); diff --git a/src/Utils/RailPathPoseHelper.cs b/src/Utils/RailPathPoseHelper.cs index 409c323..27b6fb7 100644 --- a/src/Utils/RailPathPoseHelper.cs +++ b/src/Utils/RailPathPoseHelper.cs @@ -15,6 +15,31 @@ namespace NavisworksTransport.Utils private const double TangentEpsilon = 1e-9; private static bool _rotationConstructorConventionLogged; + /// + /// 将宿主坐标系中的 Rail 首选法向统一到宿主 up 半球,避免新建装配路径把“轨上侧”写反。 + /// + public static Vector3 NormalizePreferredNormalToHostUpHemisphere(Vector3 hostPreferredNormal, Vector3 hostUpVector) + { + if (hostPreferredNormal.LengthSquared() < 1e-9f) + { + throw new ArgumentException("Rail 首选法向长度过小,无法归一化。", nameof(hostPreferredNormal)); + } + + if (hostUpVector.LengthSquared() < 1e-9f) + { + throw new ArgumentException("宿主 up 向量长度过小,无法归一化。", nameof(hostUpVector)); + } + + Vector3 normalizedPreferredNormal = Vector3.Normalize(hostPreferredNormal); + Vector3 normalizedHostUp = Vector3.Normalize(hostUpVector); + if (Vector3.Dot(normalizedPreferredNormal, normalizedHostUp) < 0f) + { + normalizedPreferredNormal = -normalizedPreferredNormal; + } + + return normalizedPreferredNormal; + } + /// /// 计算构件底面中心相对于 Rail 参考点的 Z 偏移(模型单位)。 /// diff --git a/src/Utils/ViewpointHelper.cs b/src/Utils/ViewpointHelper.cs index 68efe15..08aea47 100644 --- a/src/Utils/ViewpointHelper.cs +++ b/src/Utils/ViewpointHelper.cs @@ -301,7 +301,7 @@ namespace NavisworksTransport.Utils case ViewpointStrategy.PathHoistingSelection: return new PathViewpointProfile(distanceScale: 1.2, minDistanceMeters: 8.0, elevationDegrees: 22.0); case ViewpointStrategy.PathRailSelection: - return new PathViewpointProfile(distanceScale: 0.9, minDistanceMeters: 6.0, elevationDegrees: 12.0); + return new PathViewpointProfile(distanceScale: 0.75, minDistanceMeters: 4.5, elevationDegrees: 12.0); case ViewpointStrategy.PathGroundSelection: default: return new PathViewpointProfile(distanceScale: 1.0, minDistanceMeters: 12.0, elevationDegrees: 90.0);