Add assembly end-face guided optical axis

This commit is contained in:
tian 2026-03-22 19:45:09 +08:00
parent b1d4170334
commit 70aaff10bf
6 changed files with 786 additions and 15 deletions

View File

@ -61,6 +61,7 @@
<Compile Include="UnitTests\CoordinateSystem\HoistingCoordinateHelperTests.cs" /> <Compile Include="UnitTests\CoordinateSystem\HoistingCoordinateHelperTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\ModelAxisConventionTests.cs" /> <Compile Include="UnitTests\CoordinateSystem\ModelAxisConventionTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\ProjectReferenceFrameTests.cs" /> <Compile Include="UnitTests\CoordinateSystem\ProjectReferenceFrameTests.cs" />
<Compile Include="UnitTests\CoordinateSystem\AssemblyEndFaceAnalyzerTests.cs" />
<Compile Include="UnitTests\Properties\AssemblyInfo.cs" /> <Compile Include="UnitTests\Properties\AssemblyInfo.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -139,6 +139,7 @@
<Compile Include="src\Core\VirtualObjectManager.cs" /> <Compile Include="src\Core\VirtualObjectManager.cs" />
<!-- Core - Section Box Export --> <!-- Core - Section Box Export -->
<Compile Include="src\Core\SectionBoxExporter.cs" /> <Compile Include="src\Core\SectionBoxExporter.cs" />
<Compile Include="src\Utils\Assembly\AssemblyEndFaceAnalyzer.cs" />
<!-- Core - Configuration Management --> <!-- Core - Configuration Management -->
<Compile Include="src\Core\Config\SystemConfig.cs" /> <Compile Include="src\Core\Config\SystemConfig.cs" />
<Compile Include="src\Core\Config\ConfigManager.cs" /> <Compile Include="src\Core\Config\ConfigManager.cs" />

View File

@ -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<AnalysisTriangle3>();
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<AnalysisTriangle3>();
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<AnalysisTriangle3>();
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<AnalysisTriangle3> 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<AnalysisTriangle3> 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);
}
}
}

View File

@ -8,6 +8,7 @@ using System.Windows.Input;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Linq; using System.Linq;
using System.IO; using System.IO;
using System.Numerics;
using Microsoft.Win32; using Microsoft.Win32;
using Autodesk.Navisworks.Api; using Autodesk.Navisworks.Api;
using Autodesk.Navisworks.Api.Plugins; using Autodesk.Navisworks.Api.Plugins;
@ -19,6 +20,7 @@ using NavisworksTransport.UI.WPF.Views;
using NavisworksTransport.UI.WPF.Models; using NavisworksTransport.UI.WPF.Models;
using NavisworksTransport.Utils; using NavisworksTransport.Utils;
using NavisworksTransport.Utils.CoordinateSystem; using NavisworksTransport.Utils.CoordinateSystem;
using NavisworksTransport.Utils.GeometryAnalysis;
using NavisworksTransport; using NavisworksTransport;
namespace NavisworksTransport.UI.WPF.ViewModels namespace NavisworksTransport.UI.WPF.ViewModels
@ -138,11 +140,19 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private RailMountMode _assemblyMountMode = RailMountMode.UnderRail; private RailMountMode _assemblyMountMode = RailMountMode.UnderRail;
private bool _hasAssemblyTerminalObject; private bool _hasAssemblyTerminalObject;
private bool _isSelectingAssemblyStartPoint; private bool _isSelectingAssemblyStartPoint;
private bool _isSelectingAssemblyEndFacePoints;
private bool _hasAssemblyEndFaceAnalysis;
private ModelItem _assemblyTerminalObject; private ModelItem _assemblyTerminalObject;
private Point3D _assemblyStartPoint; private Point3D _assemblyStartPoint;
private Point3D _assemblyEndFaceCenterPoint;
private Vector3 _assemblyEndFaceNormal;
private readonly List<Point3D> _assemblyEndFaceSeedPoints = new List<Point3D>();
private const string AssemblyAnchorMarkerPathId = "assembly_anchor_marker"; private const string AssemblyAnchorMarkerPathId = "assembly_anchor_marker";
private const string AssemblyCenterGuideLinePathId = "assembly_center_guide_line"; private const string AssemblyCenterGuideLinePathId = "assembly_center_guide_line";
private const string AssemblyReferenceLinePathId = "assembly_reference_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管理 // 自动路径起点和终点的路径对象引用用于正确的ID管理
private PathRoute _autoPathStartPointRoute = null; private PathRoute _autoPathStartPointRoute = null;
@ -960,6 +970,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public ICommand GenerateAssemblyReferenceRodCommand { get; private set; } public ICommand GenerateAssemblyReferenceRodCommand { get; private set; }
public ICommand SelectAssemblyStartPointCommand { get; private set; } public ICommand SelectAssemblyStartPointCommand { get; private set; }
public ICommand ClearAssemblyReferenceRodCommand { get; private set; } public ICommand ClearAssemblyReferenceRodCommand { get; private set; }
public ICommand AnalyzeAssemblyTerminalFaceCommand { get; private set; }
// 多层吊装命令 // 多层吊装命令
public ICommand SelectMultiLevelStartPointCommand { get; private set; } public ICommand SelectMultiLevelStartPointCommand { get; private set; }
@ -1002,6 +1013,12 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_pathPlanningManager != null && _pathPlanningManager != null &&
AssemblyReferencePathManager.Instance.HasReferenceLine && AssemblyReferencePathManager.Instance.HasReferenceLine &&
!IsSelectingAssemblyStartPoint; !IsSelectingAssemblyStartPoint;
public bool CanAnalyzeAssemblyTerminalFace => HasAssemblyTerminalObject &&
_pathPlanningManager != null &&
!IsSelectingAssemblyStartPoint &&
!IsSelectingAssemblyEndFacePoints;
public bool IsSelectingAssemblyEndFacePoints => _isSelectingAssemblyEndFacePoints;
public bool CanExecuteModifyPoint => SelectedPathPoint != null && public bool CanExecuteModifyPoint => SelectedPathPoint != null &&
_pathPlanningManager?.PathEditState != PathEditState.EditingPoint; _pathPlanningManager?.PathEditState != PathEditState.EditingPoint;
@ -1285,6 +1302,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
GenerateAssemblyReferenceRodCommand = new RelayCommand(async () => await ExecuteGenerateAssemblyReferenceRodAsync(), () => CanGenerateAssemblyReferenceRod); GenerateAssemblyReferenceRodCommand = new RelayCommand(async () => await ExecuteGenerateAssemblyReferenceRodAsync(), () => CanGenerateAssemblyReferenceRod);
SelectAssemblyStartPointCommand = new RelayCommand(async () => await ExecuteSelectAssemblyStartPointAsync(), () => CanSelectAssemblyStartPoint); SelectAssemblyStartPointCommand = new RelayCommand(async () => await ExecuteSelectAssemblyStartPointAsync(), () => CanSelectAssemblyStartPoint);
ClearAssemblyReferenceRodCommand = new RelayCommand(() => ExecuteClearAssemblyReferenceRod()); ClearAssemblyReferenceRodCommand = new RelayCommand(() => ExecuteClearAssemblyReferenceRod());
AnalyzeAssemblyTerminalFaceCommand = new RelayCommand(async () => await ExecuteAnalyzeAssemblyTerminalFaceAsync(), () => CanAnalyzeAssemblyTerminalFace);
} }
#endregion #endregion
@ -1317,8 +1335,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels
InitializeAssemblyAnchorVerticalOffsetFromTerminalObject(); InitializeAssemblyAnchorVerticalOffsetFromTerminalObject();
RefreshAssemblyTerminalObjectInfo(); RefreshAssemblyTerminalObjectInfo();
RenderAssemblyAnchorMarker(); RenderAssemblyAnchorMarker();
ClearAssemblyEndFaceAnalysisVisuals();
OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod)); OnPropertyChanged(nameof(CanGenerateAssemblyReferenceRod));
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
UpdateMainStatus($"已捕获终点箱体: {AssemblyTerminalObjectName}"); UpdateMainStatus($"已捕获终点箱体: {AssemblyTerminalObjectName}");
LogManager.Info($"[直线装配] 已捕获终点箱体: {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) private async void OnAssemblyReferenceMouseClicked(object sender, PickItemResult pickResult)
{ {
try 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) private void CreateAssemblyLinearRoute(Point3D startPoint)
{ {
Point3D endPoint = AssemblyReferencePathManager.Instance.ReferenceLineEnd; Point3D endPoint = AssemblyReferencePathManager.Instance.ReferenceLineEnd;
@ -1505,15 +1604,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private void HideAssemblyReferenceVisuals(bool resetStartPointText, string statusMessage, string logMessage) private void HideAssemblyReferenceVisuals(bool resetStartPointText, string statusMessage, string logMessage)
{ {
CleanupAssemblyReferenceSelection(); CleanupAssemblyReferenceSelection();
CleanupAssemblyEndFaceSelection(clearVisuals: false);
AssemblyReferencePathManager.Instance.HideReferenceRod(); AssemblyReferencePathManager.Instance.HideReferenceRod();
ClearAssemblyAnchorMarker(); ClearAssemblyAnchorMarker();
ClearAssemblyCenterGuideLine(); ClearAssemblyCenterGuideLine();
ClearAssemblyReferenceLine(); ClearAssemblyReferenceLine();
ClearAssemblyEndFaceAnalysisVisuals();
if (resetStartPointText) if (resetStartPointText)
{ {
AssemblyStartPointText = "未选择"; AssemblyStartPointText = "未选择";
} }
OnPropertyChanged(nameof(CanSelectAssemblyStartPoint)); OnPropertyChanged(nameof(CanSelectAssemblyStartPoint));
OnPropertyChanged(nameof(CanAnalyzeAssemblyTerminalFace));
if (!string.IsNullOrWhiteSpace(statusMessage)) if (!string.IsNullOrWhiteSpace(statusMessage))
{ {
UpdateMainStatus(statusMessage); UpdateMainStatus(statusMessage);
@ -1552,6 +1654,21 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return adapter.FromCanonicalPoint(canonicalAnchorPoint); 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() private AssemblyReferenceLine BuildAssemblyReferenceLine()
{ {
if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject)) if (_assemblyTerminalObject == null || !ModelItemAnalysisHelper.IsModelItemValid(_assemblyTerminalObject))
@ -1562,20 +1679,19 @@ namespace NavisworksTransport.UI.WPF.ViewModels
HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
ProjectReferenceFrame projectFrame = CreateAssemblyProjectReferenceFrame(adapter); ProjectReferenceFrame projectFrame = CreateAssemblyProjectReferenceFrame(adapter);
BoundingBox3D bounds = _assemblyTerminalObject.BoundingBox(); Point3D opticalAxisReferencePoint = GetAssemblyOpticalAxisReferencePoint();
Point3D centerPoint = bounds.Center;
Point3D endPoint = GetAssemblyTerminalAnchorPoint(); Point3D endPoint = GetAssemblyTerminalAnchorPoint();
Point3D canonicalCenterPoint = adapter.ToCanonicalPoint(centerPoint); Point3D canonicalAxisReferencePoint = adapter.ToCanonicalPoint(opticalAxisReferencePoint);
Point3D canonicalEndPoint = adapter.ToCanonicalPoint(endPoint); Point3D canonicalEndPoint = adapter.ToCanonicalPoint(endPoint);
Vector3D direction = new Vector3D( Vector3D direction = new Vector3D(
canonicalCenterPoint.X - projectFrame.SphereCenterInCanonical.X, canonicalAxisReferencePoint.X - projectFrame.SphereCenterInCanonical.X,
canonicalCenterPoint.Y - projectFrame.SphereCenterInCanonical.Y, canonicalAxisReferencePoint.Y - projectFrame.SphereCenterInCanonical.Y,
canonicalCenterPoint.Z - projectFrame.SphereCenterInCanonical.Z); canonicalAxisReferencePoint.Z - projectFrame.SphereCenterInCanonical.Z);
double directionLengthSquared = direction.X * direction.X + direction.Y * direction.Y + direction.Z * direction.Z; double directionLengthSquared = direction.X * direction.X + direction.Y * direction.Y + direction.Z * direction.Z;
if (directionLengthSquared < 1e-9) if (directionLengthSquared < 1e-9)
{ {
throw new InvalidOperationException("箱体中心与项目球心重合,无法生成装配参考线方向"); throw new InvalidOperationException("光轴参考点与项目球心重合,无法生成装配参考线方向");
} }
direction = direction.Normalize(); direction = direction.Normalize();
@ -1588,9 +1704,9 @@ namespace NavisworksTransport.UI.WPF.ViewModels
LogManager.Info( LogManager.Info(
$"[直线装配] 参考线已计算,终点锚点=({endPoint.X:F2}, {endPoint.Y:F2}, {endPoint.Z:F2}), " + $"[直线装配] 参考线已计算,终点锚点=({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}), " + $"参考线外端=({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); return new AssemblyReferenceLine(startPoint, endPoint, direction);
} }
@ -1606,11 +1722,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
Point3D anchorPoint = GetAssemblyTerminalAnchorPoint(); Point3D anchorPoint = GetAssemblyTerminalAnchorPoint();
string anchorText = GetAssemblyAnchorText(); string anchorText = GetAssemblyAnchorText();
string mountText = AssemblyMountMode == RailMountMode.OverRail ? "轨上安装" : "轨下安装"; 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) if (referenceStartPoint != null && referenceEndPoint != null)
{ {
AssemblyTerminalObjectInfo = string.Format( 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.X,
bounds.Center.Y, bounds.Center.Y,
bounds.Center.Z, bounds.Center.Z,
@ -1622,6 +1742,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
referenceEndPoint.X, referenceEndPoint.X,
referenceEndPoint.Y, referenceEndPoint.Y,
referenceEndPoint.Z, referenceEndPoint.Z,
opticalAxisText,
AssemblyAnchorVerticalOffsetInMeters, AssemblyAnchorVerticalOffsetInMeters,
referenceStartPoint.X, referenceStartPoint.X,
referenceStartPoint.Y, referenceStartPoint.Y,
@ -1630,7 +1751,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
} }
AssemblyTerminalObjectInfo = string.Format( 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.X,
bounds.Center.Y, bounds.Center.Y,
bounds.Center.Z, bounds.Center.Z,
@ -1642,6 +1763,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
anchorPoint.X, anchorPoint.X,
anchorPoint.Y, anchorPoint.Y,
anchorPoint.Z, anchorPoint.Z,
opticalAxisText,
AssemblyAnchorVerticalOffsetInMeters); AssemblyAnchorVerticalOffsetInMeters);
} }
@ -1800,7 +1922,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
Id = AssemblyAnchorMarkerPathId, Id = AssemblyAnchorMarkerPathId,
Description = "直线装配终点对接标记" Description = "直线装配终点对接标记"
}; };
markerRoute.AddPoint(new PathPoint(anchorPoint, "对接点", PathPointType.EndPoint)); AddVisualizationPoint(markerRoute, anchorPoint, "对接点", PathPointType.EndPoint);
renderPlugin.RenderPointOnly(markerRoute); renderPlugin.RenderPointOnly(markerRoute);
} }
@ -1837,7 +1959,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter(); HostCoordinateAdapter adapter = CoordinateSystemManager.Instance.CreateHostAdapter();
ProjectReferenceFrame projectFrame = CreateAssemblyProjectReferenceFrame(adapter); ProjectReferenceFrame projectFrame = CreateAssemblyProjectReferenceFrame(adapter);
Point3D centerPoint = _assemblyTerminalObject.BoundingBox().Center; Point3D centerPoint = GetAssemblyOpticalAxisReferencePoint();
Point3D sphereCenterPoint = adapter.FromCanonicalPoint(projectFrame.SphereCenterInCanonical); Point3D sphereCenterPoint = adapter.FromCanonicalPoint(projectFrame.SphereCenterInCanonical);
renderPlugin.RenderRailBaseline( renderPlugin.RenderRailBaseline(
AssemblyCenterGuideLinePathId, AssemblyCenterGuideLinePathId,
@ -1845,8 +1967,90 @@ namespace NavisworksTransport.UI.WPF.ViewModels
RenderStyleName.AssemblyGuideLine); RenderStyleName.AssemblyGuideLine);
LogManager.Info( LogManager.Info(
$"[直线装配] 已渲染球心到箱体中心基准线: 球心=({sphereCenterPoint.X:F2}, {sphereCenterPoint.Y:F2}, {sphereCenterPoint.Z:F2}), " + $"[直线装配] 已渲染球心到光轴参考点基准线: 球心=({sphereCenterPoint.X:F2}, {sphereCenterPoint.Y:F2}, {sphereCenterPoint.Z:F2}), " +
$"箱体中心=({centerPoint.X:F2}, {centerPoint.Y:F2}, {centerPoint.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<Point3D> { centerPoint, normalEndPoint },
RenderStyleName.AssemblyGuideLine);
} }
private ProjectReferenceFrame CreateAssemblyProjectReferenceFrame(HostCoordinateAdapter adapter) private ProjectReferenceFrame CreateAssemblyProjectReferenceFrame(HostCoordinateAdapter adapter)
@ -1901,6 +2105,22 @@ namespace NavisworksTransport.UI.WPF.ViewModels
renderPlugin.ClearRailBaseline(AssemblyCenterGuideLinePathId); 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() private void CleanupAssemblyReferenceSelection()
{ {
try 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() private async Task ExecuteNewPathAsync()
{ {
await SafeExecuteAsync(() => await SafeExecuteAsync(() =>
@ -5377,6 +5678,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
LogManager.Warning($"清理直线装配事件订阅时发生异常: {ex.Message}"); LogManager.Warning($"清理直线装配事件订阅时发生异常: {ex.Message}");
} }
try
{
CleanupAssemblyEndFaceSelection(clearVisuals: true);
}
catch (Exception ex)
{
LogManager.Warning($"清理端面三点分析事件订阅时发生异常: {ex.Message}");
}
try try
{ {
ClearAssemblyAnchorMarker(); ClearAssemblyAnchorMarker();

View File

@ -462,6 +462,10 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
Command="{Binding SelectAssemblyStartPointCommand}" Command="{Binding SelectAssemblyStartPointCommand}"
Style="{StaticResource ActionButtonStyle}" Style="{StaticResource ActionButtonStyle}"
IsEnabled="{Binding CanSelectAssemblyStartPoint}"/> IsEnabled="{Binding CanSelectAssemblyStartPoint}"/>
<Button Content="分析端面(3点)"
Command="{Binding AnalyzeAssemblyTerminalFaceCommand}"
Style="{StaticResource SecondaryButtonStyle}"
IsEnabled="{Binding CanAnalyzeAssemblyTerminalFace}"/>
<Button Content="隐藏辅助线" <Button Content="隐藏辅助线"
Command="{Binding ClearAssemblyReferenceRodCommand}" Command="{Binding ClearAssemblyReferenceRodCommand}"
Style="{StaticResource SecondaryButtonStyle}"/> Style="{StaticResource SecondaryButtonStyle}"/>

View File

@ -0,0 +1,342 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Autodesk.Navisworks.Api;
namespace NavisworksTransport.Utils.GeometryAnalysis
{
/// <summary>
/// 基于端面上的三个种子点,从真实三角几何中识别端面并估计几何中心。
/// 算法核心使用纯 Vector3便于单测独立运行。
/// </summary>
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<AnalysisTriangle3> triangles = GeometryHelper.ExtractTriangles(new[] { modelItem })
.Select(ConvertTriangle)
.ToList();
return Analyze(triangles, ToVector3(seedPoint1), ToVector3(seedPoint2), ToVector3(seedPoint3));
}
public static EndFaceAnalysisResult Analyze(
IEnumerable<AnalysisTriangle3> triangles,
Vector3 seedPoint1,
Vector3 seedPoint2,
Vector3 seedPoint3)
{
if (triangles == null)
{
throw new ArgumentNullException(nameof(triangles));
}
List<AnalysisTriangle3> 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<AnalysisTriangle3>();
var candidatePoints = new HashSet<Vector3>(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<Vector3> 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<Vector3>
{
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);
}
}
}