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}"/>
+
diff --git a/src/Utils/Assembly/AssemblyEndFaceAnalyzer.cs b/src/Utils/Assembly/AssemblyEndFaceAnalyzer.cs
new file mode 100644
index 0000000..7eda516
--- /dev/null
+++ b/src/Utils/Assembly/AssemblyEndFaceAnalyzer.cs
@@ -0,0 +1,342 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using Autodesk.Navisworks.Api;
+
+namespace NavisworksTransport.Utils.GeometryAnalysis
+{
+ ///
+ /// 基于端面上的三个种子点,从真实三角几何中识别端面并估计几何中心。
+ /// 算法核心使用纯 Vector3,便于单测独立运行。
+ ///
+ public static class AssemblyEndFaceAnalyzer
+ {
+ private const double MinimumSeedTriangleAreaSquared = 1e-8;
+ private const double PlaneDistanceTolerance = 1e-3;
+ private const double NormalAlignmentCosineThreshold = 0.98;
+ private const double MinimumProjectedExtent = 1e-6;
+
+ public static EndFaceAnalysisResult Analyze(ModelItem modelItem, Point3D seedPoint1, Point3D seedPoint2, Point3D seedPoint3)
+ {
+ if (modelItem == null)
+ {
+ throw new ArgumentNullException(nameof(modelItem));
+ }
+
+ List triangles = GeometryHelper.ExtractTriangles(new[] { modelItem })
+ .Select(ConvertTriangle)
+ .ToList();
+
+ return Analyze(triangles, ToVector3(seedPoint1), ToVector3(seedPoint2), ToVector3(seedPoint3));
+ }
+
+ public static EndFaceAnalysisResult Analyze(
+ IEnumerable triangles,
+ Vector3 seedPoint1,
+ Vector3 seedPoint2,
+ Vector3 seedPoint3)
+ {
+ if (triangles == null)
+ {
+ throw new ArgumentNullException(nameof(triangles));
+ }
+
+ List triangleList = triangles.ToList();
+ if (triangleList.Count == 0)
+ {
+ return EndFaceAnalysisResult.Failure("没有可用的三角几何数据。");
+ }
+
+ PlaneDefinition seedPlane;
+ if (!TryCreatePlane(seedPoint1, seedPoint2, seedPoint3, out seedPlane))
+ {
+ return EndFaceAnalysisResult.Failure("三个端面点近似共线,无法拟合端面平面。");
+ }
+
+ var candidateTriangles = new List();
+ var candidatePoints = new HashSet(new Vector3EqualityComparer());
+ double maxPlaneDeviation = 0.0;
+
+ foreach (AnalysisTriangle3 triangle in triangleList)
+ {
+ Vector3 triangleNormal;
+ if (!TryGetTriangleNormal(triangle, out triangleNormal))
+ {
+ continue;
+ }
+
+ double normalAlignment = Math.Abs(Vector3.Dot(triangleNormal, seedPlane.Normal));
+ if (normalAlignment < NormalAlignmentCosineThreshold)
+ {
+ continue;
+ }
+
+ double distance1 = Math.Abs(GetSignedDistanceToPlane(seedPlane, triangle.Point1));
+ double distance2 = Math.Abs(GetSignedDistanceToPlane(seedPlane, triangle.Point2));
+ double distance3 = Math.Abs(GetSignedDistanceToPlane(seedPlane, triangle.Point3));
+ double triangleDeviation = Math.Max(distance1, Math.Max(distance2, distance3));
+ if (triangleDeviation > PlaneDistanceTolerance)
+ {
+ continue;
+ }
+
+ candidateTriangles.Add(triangle);
+ candidatePoints.Add(ProjectPointToPlane(seedPlane, triangle.Point1));
+ candidatePoints.Add(ProjectPointToPlane(seedPlane, triangle.Point2));
+ candidatePoints.Add(ProjectPointToPlane(seedPlane, triangle.Point3));
+ maxPlaneDeviation = Math.Max(maxPlaneDeviation, triangleDeviation);
+ }
+
+ if (candidateTriangles.Count == 0 || candidatePoints.Count < 3)
+ {
+ return EndFaceAnalysisResult.Failure("没有识别到足够的端面共面三角形。");
+ }
+
+ PlaneDefinition refinedPlane = RefinePlane(seedPlane, candidatePoints);
+ PlaneBasis planeBasis = CreatePlaneBasis(refinedPlane);
+
+ double minU = double.MaxValue;
+ double maxU = double.MinValue;
+ double minV = double.MaxValue;
+ double maxV = double.MinValue;
+
+ foreach (Vector3 candidatePoint in candidatePoints)
+ {
+ Vector2 projected = ProjectToPlane2D(candidatePoint, planeBasis);
+ minU = Math.Min(minU, projected.X);
+ maxU = Math.Max(maxU, projected.X);
+ minV = Math.Min(minV, projected.Y);
+ maxV = Math.Max(maxV, projected.Y);
+ }
+
+ double extentU = maxU - minU;
+ double extentV = maxV - minV;
+ if (extentU < MinimumProjectedExtent || extentV < MinimumProjectedExtent)
+ {
+ return EndFaceAnalysisResult.Failure("识别到的端面分布过窄,无法稳定求中心。");
+ }
+
+ Vector3 center = ProjectFromPlane2D((minU + maxU) / 2.0, (minV + maxV) / 2.0, planeBasis);
+
+ return EndFaceAnalysisResult.Success(
+ center,
+ refinedPlane.Normal,
+ candidateTriangles.Count,
+ candidatePoints.Count,
+ maxPlaneDeviation,
+ $"端面识别完成: 三角形={candidateTriangles.Count}, 顶点={candidatePoints.Count}, 偏差={maxPlaneDeviation:F6}");
+ }
+
+ public static Point3D ToPoint3D(Vector3 point)
+ {
+ return new Point3D(point.X, point.Y, point.Z);
+ }
+
+ private static AnalysisTriangle3 ConvertTriangle(Triangle3D triangle)
+ {
+ return new AnalysisTriangle3(ToVector3(triangle.Point1), ToVector3(triangle.Point2), ToVector3(triangle.Point3));
+ }
+
+ private static Vector3 ToVector3(Point3D point)
+ {
+ return new Vector3((float)point.X, (float)point.Y, (float)point.Z);
+ }
+
+ private static bool TryCreatePlane(Vector3 point1, Vector3 point2, Vector3 point3, out PlaneDefinition plane)
+ {
+ Vector3 edge1 = point2 - point1;
+ Vector3 edge2 = point3 - point1;
+ Vector3 normal = Vector3.Cross(edge1, edge2);
+ if (normal.LengthSquared() < MinimumSeedTriangleAreaSquared)
+ {
+ plane = default(PlaneDefinition);
+ return false;
+ }
+
+ Vector3 normalizedNormal = Vector3.Normalize(normal);
+ plane = new PlaneDefinition(normalizedNormal, Vector3.Dot(normalizedNormal, point1));
+ return true;
+ }
+
+ private static bool TryGetTriangleNormal(AnalysisTriangle3 triangle, out Vector3 normal)
+ {
+ Vector3 edge1 = triangle.Point2 - triangle.Point1;
+ Vector3 edge2 = triangle.Point3 - triangle.Point1;
+ Vector3 cross = Vector3.Cross(edge1, edge2);
+ if (cross.LengthSquared() < 1e-12)
+ {
+ normal = Vector3.Zero;
+ return false;
+ }
+
+ normal = Vector3.Normalize(cross);
+ return true;
+ }
+
+ private static PlaneDefinition RefinePlane(PlaneDefinition seedPlane, IEnumerable candidatePoints)
+ {
+ double averageOffset = candidatePoints.Average(point => Vector3.Dot(seedPlane.Normal, point));
+ return new PlaneDefinition(seedPlane.Normal, averageOffset);
+ }
+
+ private static double GetSignedDistanceToPlane(PlaneDefinition plane, Vector3 point)
+ {
+ return Vector3.Dot(plane.Normal, point) - plane.Offset;
+ }
+
+ private static Vector3 ProjectPointToPlane(PlaneDefinition plane, Vector3 point)
+ {
+ float signedDistance = (float)GetSignedDistanceToPlane(plane, point);
+ return point - plane.Normal * signedDistance;
+ }
+
+ private static PlaneBasis CreatePlaneBasis(PlaneDefinition plane)
+ {
+ Vector3 reference = Math.Abs(plane.Normal.Z) < 0.9f
+ ? new Vector3(0.0f, 0.0f, 1.0f)
+ : new Vector3(1.0f, 0.0f, 0.0f);
+ Vector3 axisU = Vector3.Normalize(Vector3.Cross(reference, plane.Normal));
+ Vector3 axisV = Vector3.Normalize(Vector3.Cross(plane.Normal, axisU));
+ Vector3 origin = plane.Normal * (float)plane.Offset;
+ return new PlaneBasis(origin, axisU, axisV);
+ }
+
+ private static Vector2 ProjectToPlane2D(Vector3 point, PlaneBasis basis)
+ {
+ Vector3 vector = point - basis.Origin;
+ return new Vector2(Vector3.Dot(vector, basis.AxisU), Vector3.Dot(vector, basis.AxisV));
+ }
+
+ private static Vector3 ProjectFromPlane2D(double u, double v, PlaneBasis basis)
+ {
+ return basis.Origin + basis.AxisU * (float)u + basis.AxisV * (float)v;
+ }
+
+ private struct PlaneDefinition
+ {
+ public PlaneDefinition(Vector3 normal, double offset)
+ {
+ Normal = normal;
+ Offset = offset;
+ }
+
+ public Vector3 Normal { get; }
+ public double Offset { get; }
+ }
+
+ private struct PlaneBasis
+ {
+ public PlaneBasis(Vector3 origin, Vector3 axisU, Vector3 axisV)
+ {
+ Origin = origin;
+ AxisU = axisU;
+ AxisV = axisV;
+ }
+
+ public Vector3 Origin { get; }
+ public Vector3 AxisU { get; }
+ public Vector3 AxisV { get; }
+ }
+
+ private sealed class Vector3EqualityComparer : IEqualityComparer
+ {
+ private const float Tolerance = 1e-5f;
+
+ public bool Equals(Vector3 x, Vector3 y)
+ {
+ return Math.Abs(x.X - y.X) <= Tolerance &&
+ Math.Abs(x.Y - y.Y) <= Tolerance &&
+ Math.Abs(x.Z - y.Z) <= Tolerance;
+ }
+
+ public int GetHashCode(Vector3 obj)
+ {
+ int x = (int)Math.Round(obj.X / Tolerance);
+ int y = (int)Math.Round(obj.Y / Tolerance);
+ int z = (int)Math.Round(obj.Z / Tolerance);
+ unchecked
+ {
+ int hash = 17;
+ hash = hash * 31 + x;
+ hash = hash * 31 + y;
+ hash = hash * 31 + z;
+ return hash;
+ }
+ }
+ }
+ }
+
+ public struct AnalysisTriangle3
+ {
+ public AnalysisTriangle3(Vector3 point1, Vector3 point2, Vector3 point3)
+ {
+ Point1 = point1;
+ Point2 = point2;
+ Point3 = point3;
+ }
+
+ public Vector3 Point1 { get; }
+ public Vector3 Point2 { get; }
+ public Vector3 Point3 { get; }
+ }
+
+ public sealed class EndFaceAnalysisResult
+ {
+ private EndFaceAnalysisResult(
+ bool isReliable,
+ Vector3 center,
+ Vector3 normal,
+ int candidateTriangleCount,
+ int candidateVertexCount,
+ double maxPlaneDeviation,
+ string diagnosticMessage)
+ {
+ IsReliable = isReliable;
+ Center = center;
+ Normal = normal;
+ CandidateTriangleCount = candidateTriangleCount;
+ CandidateVertexCount = candidateVertexCount;
+ MaxPlaneDeviation = maxPlaneDeviation;
+ DiagnosticMessage = diagnosticMessage;
+ }
+
+ public bool IsReliable { get; }
+ public Vector3 Center { get; }
+ public Vector3 Normal { get; }
+ public int CandidateTriangleCount { get; }
+ public int CandidateVertexCount { get; }
+ public double MaxPlaneDeviation { get; }
+ public string DiagnosticMessage { get; }
+
+ public static EndFaceAnalysisResult Success(
+ Vector3 center,
+ Vector3 normal,
+ int candidateTriangleCount,
+ int candidateVertexCount,
+ double maxPlaneDeviation,
+ string diagnosticMessage)
+ {
+ return new EndFaceAnalysisResult(
+ true,
+ center,
+ normal,
+ candidateTriangleCount,
+ candidateVertexCount,
+ maxPlaneDeviation,
+ diagnosticMessage);
+ }
+
+ public static EndFaceAnalysisResult Failure(string diagnosticMessage)
+ {
+ return new EndFaceAnalysisResult(
+ false,
+ Vector3.Zero,
+ Vector3.Zero,
+ 0,
+ 0,
+ 0.0,
+ diagnosticMessage);
+ }
+ }
+}