diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj index 8cd57ca..eb50839 100644 --- a/NavisworksTransport.UnitTests.csproj +++ b/NavisworksTransport.UnitTests.csproj @@ -61,6 +61,7 @@ + diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj index 9ab3261..bc1bef4 100644 --- a/TransportPlugin.csproj +++ b/TransportPlugin.csproj @@ -139,6 +139,7 @@ + diff --git a/UnitTests/CoordinateSystem/AssemblyEndFaceAnalyzerTests.cs b/UnitTests/CoordinateSystem/AssemblyEndFaceAnalyzerTests.cs new file mode 100644 index 0000000..4824dfc --- /dev/null +++ b/UnitTests/CoordinateSystem/AssemblyEndFaceAnalyzerTests.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NavisworksTransport.Utils.GeometryAnalysis; + +namespace NavisworksTransport.UnitTests.CoordinateSystem +{ + [TestClass] + public class AssemblyEndFaceAnalyzerTests + { + [TestMethod] + public void Analyze_RectangularFaceWithSideNoise_ShouldReturnFaceCenter() + { + var triangles = new List(); + triangles.AddRange(CreateRectangleFace(-2.0, 2.0, -1.0, 1.0, 10.0)); + triangles.AddRange(CreateRectangleFace(-2.0, 2.0, -1.0, 1.0, 8.0)); + triangles.Add(new AnalysisTriangle3( + new Vector3(-2.0f, -1.0f, 10.0f), + new Vector3(-2.0f, -1.0f, 9.0f), + new Vector3(-2.0f, 1.0f, 9.0f))); + + EndFaceAnalysisResult result = AssemblyEndFaceAnalyzer.Analyze( + triangles, + new Vector3(-1.5f, -0.7f, 10.0f), + new Vector3(1.4f, -0.5f, 10.0f), + new Vector3(1.1f, 0.8f, 10.0f)); + + Assert.IsTrue(result.IsReliable, result.DiagnosticMessage); + AssertPoint(result.Center, 0.0, 0.0, 10.0); + Assert.AreEqual(2, result.CandidateTriangleCount); + } + + [TestMethod] + public void Analyze_SquareRingFace_ShouldReturnRingCenter() + { + var triangles = new List(); + triangles.AddRange(CreateRingFace(4.0, 2.0, 5.0)); + + EndFaceAnalysisResult result = AssemblyEndFaceAnalyzer.Analyze( + triangles, + new Vector3(-3.5f, -3.5f, 5.0f), + new Vector3(3.5f, -3.5f, 5.0f), + new Vector3(3.5f, 3.5f, 5.0f)); + + Assert.IsTrue(result.IsReliable, result.DiagnosticMessage); + AssertPoint(result.Center, 0.0, 0.0, 5.0); + Assert.IsTrue(result.CandidateTriangleCount >= 8); + } + + [TestMethod] + public void Analyze_NearlyCollinearSeedPoints_ShouldFail() + { + var triangles = new List(); + triangles.AddRange(CreateRectangleFace(-1.0, 1.0, -1.0, 1.0, 0.0)); + + EndFaceAnalysisResult result = AssemblyEndFaceAnalyzer.Analyze( + triangles, + new Vector3(0.0f, 0.0f, 0.0f), + new Vector3(1.0f, 0.0f, 0.0f), + new Vector3(2.0f, 0.0f, 0.0f)); + + Assert.IsFalse(result.IsReliable); + } + + private static IEnumerable CreateRectangleFace(double minX, double maxX, double minY, double maxY, double z) + { + yield return new AnalysisTriangle3( + new Vector3((float)minX, (float)minY, (float)z), + new Vector3((float)maxX, (float)minY, (float)z), + new Vector3((float)maxX, (float)maxY, (float)z)); + yield return new AnalysisTriangle3( + new Vector3((float)minX, (float)minY, (float)z), + new Vector3((float)maxX, (float)maxY, (float)z), + new Vector3((float)minX, (float)maxY, (float)z)); + } + + private static IEnumerable CreateRingFace(double outerHalfSize, double innerHalfSize, double z) + { + if (innerHalfSize >= outerHalfSize) + { + throw new ArgumentOutOfRangeException(nameof(innerHalfSize)); + } + + foreach (AnalysisTriangle3 triangle in CreateRectangleFace(-outerHalfSize, outerHalfSize, innerHalfSize, outerHalfSize, z)) + { + yield return triangle; + } + + foreach (AnalysisTriangle3 triangle in CreateRectangleFace(-outerHalfSize, outerHalfSize, -outerHalfSize, -innerHalfSize, z)) + { + yield return triangle; + } + + foreach (AnalysisTriangle3 triangle in CreateRectangleFace(-outerHalfSize, -innerHalfSize, -innerHalfSize, innerHalfSize, z)) + { + yield return triangle; + } + + foreach (AnalysisTriangle3 triangle in CreateRectangleFace(innerHalfSize, outerHalfSize, -innerHalfSize, innerHalfSize, z)) + { + yield return triangle; + } + } + + private static void AssertPoint(Vector3 actual, double x, double y, double z) + { + Assert.AreEqual(x, actual.X, 1e-5); + Assert.AreEqual(y, actual.Y, 1e-5); + Assert.AreEqual(z, actual.Z, 1e-5); + } + } +} diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index 1d04ecd..ef84caa 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -8,6 +8,7 @@ using System.Windows.Input; using System.Threading.Tasks; using System.Linq; using System.IO; +using System.Numerics; using Microsoft.Win32; using Autodesk.Navisworks.Api; using Autodesk.Navisworks.Api.Plugins; @@ -19,6 +20,7 @@ using NavisworksTransport.UI.WPF.Views; using NavisworksTransport.UI.WPF.Models; using NavisworksTransport.Utils; using NavisworksTransport.Utils.CoordinateSystem; +using NavisworksTransport.Utils.GeometryAnalysis; using NavisworksTransport; namespace NavisworksTransport.UI.WPF.ViewModels @@ -138,11 +140,19 @@ namespace NavisworksTransport.UI.WPF.ViewModels private RailMountMode _assemblyMountMode = RailMountMode.UnderRail; private bool _hasAssemblyTerminalObject; private bool _isSelectingAssemblyStartPoint; + private bool _isSelectingAssemblyEndFacePoints; + private bool _hasAssemblyEndFaceAnalysis; private ModelItem _assemblyTerminalObject; private Point3D _assemblyStartPoint; + private Point3D _assemblyEndFaceCenterPoint; + private Vector3 _assemblyEndFaceNormal; + private readonly List _assemblyEndFaceSeedPoints = new List(); private const string AssemblyAnchorMarkerPathId = "assembly_anchor_marker"; private const string AssemblyCenterGuideLinePathId = "assembly_center_guide_line"; private const string AssemblyReferenceLinePathId = "assembly_reference_line"; + private const string AssemblyEndFaceSeedPathId = "assembly_end_face_seed_points"; + private const string AssemblyEndFaceCenterPathId = "assembly_end_face_center"; + private const string AssemblyEndFaceNormalPathId = "assembly_end_face_normal"; // 自动路径起点和终点的路径对象引用(用于正确的ID管理) private PathRoute _autoPathStartPointRoute = null; @@ -960,6 +970,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels public ICommand GenerateAssemblyReferenceRodCommand { get; private set; } public ICommand SelectAssemblyStartPointCommand { get; private set; } public ICommand ClearAssemblyReferenceRodCommand { get; private set; } + public ICommand AnalyzeAssemblyTerminalFaceCommand { get; private set; } // 多层吊装命令 public ICommand SelectMultiLevelStartPointCommand { get; private set; } @@ -1002,6 +1013,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels _pathPlanningManager != null && AssemblyReferencePathManager.Instance.HasReferenceLine && !IsSelectingAssemblyStartPoint; + public bool CanAnalyzeAssemblyTerminalFace => HasAssemblyTerminalObject && + _pathPlanningManager != null && + !IsSelectingAssemblyStartPoint && + !IsSelectingAssemblyEndFacePoints; + + public bool IsSelectingAssemblyEndFacePoints => _isSelectingAssemblyEndFacePoints; public bool CanExecuteModifyPoint => SelectedPathPoint != null && _pathPlanningManager?.PathEditState != PathEditState.EditingPoint; @@ -1285,6 +1302,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels GenerateAssemblyReferenceRodCommand = new RelayCommand(async () => await ExecuteGenerateAssemblyReferenceRodAsync(), () => CanGenerateAssemblyReferenceRod); SelectAssemblyStartPointCommand = new RelayCommand(async () => await ExecuteSelectAssemblyStartPointAsync(), () => CanSelectAssemblyStartPoint); ClearAssemblyReferenceRodCommand = new RelayCommand(() => ExecuteClearAssemblyReferenceRod()); + AnalyzeAssemblyTerminalFaceCommand = new RelayCommand(async () => await ExecuteAnalyzeAssemblyTerminalFaceAsync(), () => CanAnalyzeAssemblyTerminalFace); } #endregion @@ -1317,8 +1335,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels InitializeAssemblyAnchorVerticalOffsetFromTerminalObject(); RefreshAssemblyTerminalObjectInfo(); RenderAssemblyAnchorMarker(); + ClearAssemblyEndFaceAnalysisVisuals(); OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod)); OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); + OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace)); UpdateMainStatus($"已捕获终点箱体: {AssemblyTerminalObjectName}"); LogManager.Info($"[直线装配] 已捕获终点箱体: {AssemblyTerminalObjectName}"); }, "捕获终点箱体"); @@ -1414,6 +1434,44 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } + private async Task ExecuteAnalyzeAssemblyTerminalFaceAsync() + { + await SafeExecuteAsync(() => + { + if (_pathPlanningManager == null) + { + throw new InvalidOperationException("路径规划管理器未初始化,无法分析端面。"); + } + + if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) + { + throw new InvalidOperationException("终点箱体未设置或已失效,请先捕获终点箱体。"); + } + + CleanupAssemblyReferenceSelection(); + CleanupAssemblyEndFaceSelection(clearVisuals: false); + ClearAssemblyEndFaceAnalysisVisuals(); + _assemblyEndFaceSeedPoints.Clear(); + + _pathPlanningManager.DisableMouseHandling(); + _isSelectingAssemblyEndFacePoints = true; + OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace)); + OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); + + PathClickToolPlugin.MouseClicked -= OnAssemblyEndFaceMouseClicked; + PathClickToolPlugin.MouseClicked += OnAssemblyEndFaceMouseClicked; + + if (!ForceReinitializeToolPlugin(subscribeToEvents: false)) + { + CleanupAssemblyEndFaceSelection(clearVisuals: false); + throw new InvalidOperationException("ToolPlugin 初始化失败,请重试。"); + } + + UpdateMainStatus("请在同一个端面平面上连续点击 3 个点,系统将分析端面中心。"); + LogManager.Info("[直线装配] 已进入端面三点分析模式"); + }, "分析终端端面"); + } + private async void OnAssemblyReferenceMouseClicked(object sender, PickItemResult pickResult) { try @@ -1445,6 +1503,47 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } + private async void OnAssemblyEndFaceMouseClicked(object sender, PickItemResult pickResult) + { + try + { + if (!_isSelectingAssemblyEndFacePoints || pickResult == null) + { + return; + } + + await SafeExecuteAsync(() => + { + if (!IsPickOnAssemblyTerminalObject(pickResult)) + { + UpdateMainStatus("请点击当前终点箱体的同一个端面,不要点到其他对象。"); + return; + } + + _assemblyEndFaceSeedPoints.Add(pickResult.Point); + RenderAssemblyEndFaceSeedPoints(); + + int pickedCount = _assemblyEndFaceSeedPoints.Count; + LogManager.Info($"[直线装配] 已记录端面种子点 {pickedCount}: ({pickResult.Point.X:F3}, {pickResult.Point.Y:F3}, {pickResult.Point.Z:F3})"); + + if (pickedCount < 3) + { + UpdateMainStatus($"已记录端面点 {pickedCount}/3,请继续在同一端面平面上点击。"); + return; + } + + AnalyzeCurrentAssemblyEndFace(); + CleanupAssemblyEndFaceSelection(clearVisuals: false); + }, "处理端面三点拾取"); + } + catch (Exception ex) + { + LogManager.Error($"[直线装配] 端面三点分析失败: {ex.Message}", ex); + UpdateMainStatus($"端面三点分析失败: {ex.Message}"); + CleanupAssemblyEndFaceSelection(clearVisuals: false); + } + } + private void CreateAssemblyLinearRoute(Point3D startPoint) { Point3D endPoint = AssemblyReferencePathManager.Instance.ReferenceLineEnd; @@ -1505,15 +1604,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels private void HideAssemblyReferenceVisuals(bool resetStartPointText, string statusMessage, string logMessage) { CleanupAssemblyReferenceSelection(); + CleanupAssemblyEndFaceSelection(clearVisuals: false); AssemblyReferencePathManager.Instance.HideReferenceRod(); ClearAssemblyAnchorMarker(); ClearAssemblyCenterGuideLine(); ClearAssemblyReferenceLine(); + ClearAssemblyEndFaceAnalysisVisuals(); if (resetStartPointText) { AssemblyStartPointText = "未选择"; } OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); + OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace)); if (!string.IsNullOrWhiteSpace(statusMessage)) { UpdateMainStatus(statusMessage); @@ -1552,6 +1654,21 @@ namespace NavisworksTransport.UI.WPF.ViewModels return adapter.FromCanonicalPoint(canonicalAnchorPoint); } + private Point3D GetAssemblyOpticalAxisReferencePoint() + { + if (_hasAssemblyEndFaceAnalysis && _assemblyEndFaceCenterPoint != null) + { + return _assemblyEndFaceCenterPoint; + } + + if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) + { + throw new InvalidOperationException("终点箱体未设置或已失效,无法计算光轴参考点"); + } + + return _assemblyTerminalObject.BoundingBox().Center; + } + private AssemblyReferenceLine BuildAssemblyReferenceLine() { if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) @@ -1562,20 +1679,19 @@ namespace NavisworksTransport.UI.WPF.ViewModels HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); ProjectReferenceFrame projectFrame = CreateAssemblyProjectReferenceFrame(adapter); - BoundingBox3D bounds = _assemblyTerminalObject.BoundingBox(); - Point3D centerPoint = bounds.Center; + Point3D opticalAxisReferencePoint = GetAssemblyOpticalAxisReferencePoint(); Point3D endPoint = GetAssemblyTerminalAnchorPoint(); - Point3D canonicalCenterPoint = adapter.ToCanonicalPoint(centerPoint); + Point3D canonicalAxisReferencePoint = adapter.ToCanonicalPoint(opticalAxisReferencePoint); Point3D canonicalEndPoint = adapter.ToCanonicalPoint(endPoint); Vector3D direction = new Vector3D( - canonicalCenterPoint.X - projectFrame.SphereCenterInCanonical.X, - canonicalCenterPoint.Y - projectFrame.SphereCenterInCanonical.Y, - canonicalCenterPoint.Z - projectFrame.SphereCenterInCanonical.Z); + canonicalAxisReferencePoint.X - projectFrame.SphereCenterInCanonical.X, + canonicalAxisReferencePoint.Y - projectFrame.SphereCenterInCanonical.Y, + canonicalAxisReferencePoint.Z - projectFrame.SphereCenterInCanonical.Z); double directionLengthSquared = direction.X * direction.X + direction.Y * direction.Y + direction.Z * direction.Z; if (directionLengthSquared < 1e-9) { - throw new InvalidOperationException("箱体中心与项目球心重合,无法生成装配参考线方向"); + throw new InvalidOperationException("光轴参考点与项目球心重合,无法生成装配参考线方向"); } direction = direction.Normalize(); @@ -1588,9 +1704,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels LogManager.Info( $"[直线装配] 参考线已计算,终点锚点=({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2}), " + - $"箱体中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2}), " + + $"光轴参考点=({opticalAxisReferencePoint.X:F2}, {opticalAxisReferencePoint.Y:F2}, {opticalAxisReferencePoint.Z:F2}), " + $"参考线外端=({startPoint.X:F2}, {startPoint.Y:F2}, {startPoint.Z:F2}), " + - $"球心到箱体中心方向(内部坐标)=({direction.X:F3}, {direction.Y:F3}, {direction.Z:F3})"); + $"球心到光轴参考点方向(内部坐标)=({direction.X:F3}, {direction.Y:F3}, {direction.Z:F3})"); return new AssemblyReferenceLine(startPoint, endPoint, direction); } @@ -1606,11 +1722,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels Point3D anchorPoint = GetAssemblyTerminalAnchorPoint(); string anchorText = GetAssemblyAnchorText(); string mountText = AssemblyMountMode == RailMountMode.OverRail ? "轨上安装" : "轨下安装"; + Point3D opticalAxisReferencePoint = GetAssemblyOpticalAxisReferencePoint(); + string opticalAxisText = _hasAssemblyEndFaceAnalysis + ? string.Format("端面中心=({0:F2}, {1:F2}, {2:F2})", opticalAxisReferencePoint.X, opticalAxisReferencePoint.Y, opticalAxisReferencePoint.Z) + : string.Format("箱体中心=({0:F2}, {1:F2}, {2:F2})", opticalAxisReferencePoint.X, opticalAxisReferencePoint.Y, opticalAxisReferencePoint.Z); if (referenceStartPoint != null && referenceEndPoint != null) { AssemblyTerminalObjectInfo = string.Format( - "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),垂直偏移={11:F3}m,辅助线外端=({12:F2}, {13:F2}, {14:F2})", + "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),{11},垂直偏移={12:F3}m,辅助线外端=({13:F2}, {14:F2}, {15:F2})", bounds.Center.X, bounds.Center.Y, bounds.Center.Z, @@ -1622,6 +1742,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels referenceEndPoint.X, referenceEndPoint.Y, referenceEndPoint.Z, + opticalAxisText, AssemblyAnchorVerticalOffsetInMeters, referenceStartPoint.X, referenceStartPoint.Y, @@ -1630,7 +1751,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels } AssemblyTerminalObjectInfo = string.Format( - "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),垂直偏移={11:F3}m", + "中心=({0:F2}, {1:F2}, {2:F2}),尺寸=({3:F2}, {4:F2}, {5:F2}),{6},{7}点=({8:F2}, {9:F2}, {10:F2}),{11},垂直偏移={12:F3}m", bounds.Center.X, bounds.Center.Y, bounds.Center.Z, @@ -1642,6 +1763,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels anchorPoint.X, anchorPoint.Y, anchorPoint.Z, + opticalAxisText, AssemblyAnchorVerticalOffsetInMeters); } @@ -1800,7 +1922,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels Id = AssemblyAnchorMarkerPathId, Description = "直线装配终点对接标记" }; - markerRoute.AddPoint(new PathPoint(anchorPoint, "对接点", PathPointType.EndPoint)); + AddVisualizationPoint(markerRoute, anchorPoint, "对接点", PathPointType.EndPoint); renderPlugin.RenderPointOnly(markerRoute); } @@ -1837,7 +1959,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); ProjectReferenceFrame projectFrame = CreateAssemblyProjectReferenceFrame(adapter); - Point3D centerPoint = _assemblyTerminalObject.BoundingBox().Center; + Point3D centerPoint = GetAssemblyOpticalAxisReferencePoint(); Point3D sphereCenterPoint = adapter.FromCanonicalPoint(projectFrame.SphereCenterInCanonical); renderPlugin.RenderRailBaseline( AssemblyCenterGuideLinePathId, @@ -1845,8 +1967,90 @@ namespace NavisworksTransport.UI.WPF.ViewModels RenderStyleName.AssemblyGuideLine); LogManager.Info( - $"[直线装配] 已渲染球心到箱体中心基准线: 球心=({sphereCenterPoint.X:F2}, {sphereCenterPoint.Y:F2}, {sphereCenterPoint.Z:F2}), " + - $"箱体中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2})"); + $"[直线装配] 已渲染球心到光轴参考点基准线: 球心=({sphereCenterPoint.X:F2}, {sphereCenterPoint.Y:F2}, {sphereCenterPoint.Z:F2}), " + + $"光轴参考点=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2})"); + } + + private void RenderAssemblyEndFaceSeedPoints() + { + var renderPlugin = PathPointRenderPlugin.Instance; + if (renderPlugin == null) + { + return; + } + + renderPlugin.RemovePath(AssemblyEndFaceSeedPathId); + + if (_assemblyEndFaceSeedPoints.Count == 0) + { + return; + } + + var markerRoute = new PathRoute("端面三点") + { + Id = AssemblyEndFaceSeedPathId, + Description = "终端安装端面三点" + }; + + for (int i = 0; i < _assemblyEndFaceSeedPoints.Count; i++) + { + AddVisualizationPoint(markerRoute, _assemblyEndFaceSeedPoints[i], $"端面点{i + 1}", PathPointType.WayPoint); + } + + renderPlugin.RenderPointOnly(markerRoute); + } + + private void RenderAssemblyEndFaceCenter(Point3D centerPoint) + { + var renderPlugin = PathPointRenderPlugin.Instance; + if (renderPlugin == null) + { + return; + } + + renderPlugin.RemovePath(AssemblyEndFaceCenterPathId); + + var markerRoute = new PathRoute("端面中心") + { + Id = AssemblyEndFaceCenterPathId, + Description = "终端安装端面中心" + }; + AddVisualizationPoint(markerRoute, centerPoint, "端面中心", PathPointType.EndPoint); + renderPlugin.RenderPointOnly(markerRoute); + } + + private static void AddVisualizationPoint(PathRoute route, Point3D position, string name, PathPointType type) + { + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } + + route.Points.Add(new PathPoint(position, name, type) + { + Index = route.Points.Count + }); + } + + private void RenderAssemblyEndFaceNormal(Point3D centerPoint, Vector3 normal) + { + var renderPlugin = PathPointRenderPlugin.Instance; + if (renderPlugin == null) + { + return; + } + + renderPlugin.ClearRailBaseline(AssemblyEndFaceNormalPathId); + + double lineLength = UnitsConverter.ConvertFromMeters(Math.Max(0.5, AssemblyReferenceRodDiameterInMeters * 4.0)); + Point3D normalEndPoint = new Point3D( + centerPoint.X + normal.X * (float)lineLength, + centerPoint.Y + normal.Y * (float)lineLength, + centerPoint.Z + normal.Z * (float)lineLength); + renderPlugin.RenderRailBaseline( + AssemblyEndFaceNormalPathId, + new List { centerPoint, normalEndPoint }, + RenderStyleName.AssemblyGuideLine); } private ProjectReferenceFrame CreateAssemblyProjectReferenceFrame(HostCoordinateAdapter adapter) @@ -1901,6 +2105,22 @@ namespace NavisworksTransport.UI.WPF.ViewModels renderPlugin.ClearRailBaseline(AssemblyCenterGuideLinePathId); } + private void ClearAssemblyEndFaceAnalysisVisuals() + { + var renderPlugin = PathPointRenderPlugin.Instance; + if (renderPlugin == null) + { + return; + } + + renderPlugin.RemovePath(AssemblyEndFaceSeedPathId); + renderPlugin.RemovePath(AssemblyEndFaceCenterPathId); + renderPlugin.ClearRailBaseline(AssemblyEndFaceNormalPathId); + _hasAssemblyEndFaceAnalysis = false; + _assemblyEndFaceCenterPoint = null; + _assemblyEndFaceNormal = default(Vector3); + } + private void CleanupAssemblyReferenceSelection() { try @@ -1917,6 +2137,87 @@ namespace NavisworksTransport.UI.WPF.ViewModels } } + private void CleanupAssemblyEndFaceSelection(bool clearVisuals) + { + try + { + PathClickToolPlugin.MouseClicked -= OnAssemblyEndFaceMouseClicked; + _isSelectingAssemblyEndFacePoints = false; + _pathPlanningManager?.EnableMouseHandling(); + _pathPlanningManager?.StopClickTool(); + if (clearVisuals) + { + _assemblyEndFaceSeedPoints.Clear(); + ClearAssemblyEndFaceAnalysisVisuals(); + } + OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace)); + OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); + } + catch (Exception ex) + { + LogManager.Error($"[直线装配] 清理端面三点拾取状态失败: {ex.Message}", ex); + } + } + + private bool IsPickOnAssemblyTerminalObject(PickItemResult pickResult) + { + if (pickResult?.ModelItem == null || _assemblyTerminalObject == null) + { + return false; + } + + if (ModelItemAnalysisHelper.ModelItemEquals(pickResult.ModelItem, _assemblyTerminalObject)) + { + return true; + } + + return pickResult.ModelItem.AncestorsAndSelf.Any(ancestor => ModelItemAnalysisHelper.ModelItemEquals(ancestor, _assemblyTerminalObject)); + } + + private void AnalyzeCurrentAssemblyEndFace() + { + if (_assemblyEndFaceSeedPoints.Count != 3) + { + throw new InvalidOperationException("端面分析需要恰好 3 个种子点。"); + } + + var triangles = GeometryHelper.ExtractTriangles(new[] { _assemblyTerminalObject }) + .Select(triangle => new AnalysisTriangle3( + new Vector3((float)triangle.Point1.X, (float)triangle.Point1.Y, (float)triangle.Point1.Z), + new Vector3((float)triangle.Point2.X, (float)triangle.Point2.Y, (float)triangle.Point2.Z), + new Vector3((float)triangle.Point3.X, (float)triangle.Point3.Y, (float)triangle.Point3.Z))) + .ToList(); + + EndFaceAnalysisResult result = AssemblyEndFaceAnalyzer.Analyze( + triangles, + new Vector3((float)_assemblyEndFaceSeedPoints[0].X, (float)_assemblyEndFaceSeedPoints[0].Y, (float)_assemblyEndFaceSeedPoints[0].Z), + new Vector3((float)_assemblyEndFaceSeedPoints[1].X, (float)_assemblyEndFaceSeedPoints[1].Y, (float)_assemblyEndFaceSeedPoints[1].Z), + new Vector3((float)_assemblyEndFaceSeedPoints[2].X, (float)_assemblyEndFaceSeedPoints[2].Y, (float)_assemblyEndFaceSeedPoints[2].Z)); + + if (!result.IsReliable) + { + throw new InvalidOperationException(result.DiagnosticMessage); + } + + Point3D centerPoint = AssemblyEndFaceAnalyzer.ToPoint3D(result.Center); + _hasAssemblyEndFaceAnalysis = true; + _assemblyEndFaceCenterPoint = centerPoint; + _assemblyEndFaceNormal = result.Normal; + RenderAssemblyEndFaceSeedPoints(); + RenderAssemblyEndFaceCenter(centerPoint); + RenderAssemblyEndFaceNormal(centerPoint, result.Normal); + RefreshAssemblyTerminalObjectInfo(); + RefreshAssemblyReferenceRodIfNeeded(); + + AssemblyTerminalObjectInfo = + $"{AssemblyTerminalObjectInfo} | 端面中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.Z:F2}),候选三角形={result.CandidateTriangleCount},偏差={result.MaxPlaneDeviation:F6}"; + 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}), " + + $"三角形={result.CandidateTriangleCount}, 顶点={result.CandidateVertexCount}, 偏差={result.MaxPlaneDeviation:F6}"); + } + private async Task ExecuteNewPathAsync() { await SafeExecuteAsync(() => @@ -5377,6 +5678,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels LogManager.Warning($"清理直线装配事件订阅时发生异常: {ex.Message}"); } + try + { + CleanupAssemblyEndFaceSelection(clearVisuals: true); + } + catch (Exception ex) + { + LogManager.Warning($"清理端面三点分析事件订阅时发生异常: {ex.Message}"); + } + try { ClearAssemblyAnchorMarker(); diff --git a/src/UI/WPF/Views/PathEditingView.xaml b/src/UI/WPF/Views/PathEditingView.xaml index 6c14832..0cea737 100644 --- a/src/UI/WPF/Views/PathEditingView.xaml +++ b/src/UI/WPF/Views/PathEditingView.xaml @@ -462,6 +462,10 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管 Command="{Binding SelectAssemblyStartPointCommand}" Style="{StaticResource ActionButtonStyle}" IsEnabled="{Binding CanSelectAssemblyStartPoint}"/> +