Add assembly end-face guided optical axis
This commit is contained in:
parent
b1d4170334
commit
70aaff10bf
@ -61,6 +61,7 @@
|
||||
<Compile Include="UnitTests\CoordinateSystem\HoistingCoordinateHelperTests.cs" />
|
||||
<Compile Include="UnitTests\CoordinateSystem\ModelAxisConventionTests.cs" />
|
||||
<Compile Include="UnitTests\CoordinateSystem\ProjectReferenceFrameTests.cs" />
|
||||
<Compile Include="UnitTests\CoordinateSystem\AssemblyEndFaceAnalyzerTests.cs" />
|
||||
<Compile Include="UnitTests\Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@ -139,6 +139,7 @@
|
||||
<Compile Include="src\Core\VirtualObjectManager.cs" />
|
||||
<!-- Core - Section Box Export -->
|
||||
<Compile Include="src\Core\SectionBoxExporter.cs" />
|
||||
<Compile Include="src\Utils\Assembly\AssemblyEndFaceAnalyzer.cs" />
|
||||
<!-- Core - Configuration Management -->
|
||||
<Compile Include="src\Core\Config\SystemConfig.cs" />
|
||||
<Compile Include="src\Core\Config\ConfigManager.cs" />
|
||||
|
||||
113
UnitTests/CoordinateSystem/AssemblyEndFaceAnalyzerTests.cs
Normal file
113
UnitTests/CoordinateSystem/AssemblyEndFaceAnalyzerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<Point3D> _assemblyEndFaceSeedPoints = new List<Point3D>();
|
||||
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<Point3D> { 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();
|
||||
|
||||
@ -462,6 +462,10 @@ NavisworksTransport 路径编辑页签视图 - 采用与动画控制和分层管
|
||||
Command="{Binding SelectAssemblyStartPointCommand}"
|
||||
Style="{StaticResource ActionButtonStyle}"
|
||||
IsEnabled="{Binding CanSelectAssemblyStartPoint}"/>
|
||||
<Button Content="分析端面(3点)"
|
||||
Command="{Binding AnalyzeAssemblyTerminalFaceCommand}"
|
||||
Style="{StaticResource SecondaryButtonStyle}"
|
||||
IsEnabled="{Binding CanAnalyzeAssemblyTerminalFace}"/>
|
||||
<Button Content="隐藏辅助线"
|
||||
Command="{Binding ClearAssemblyReferenceRodCommand}"
|
||||
Style="{StaticResource SecondaryButtonStyle}"/>
|
||||
|
||||
342
src/Utils/Assembly/AssemblyEndFaceAnalyzer.cs
Normal file
342
src/Utils/Assembly/AssemblyEndFaceAnalyzer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user