- New Analyze(triangles, hitPoint, normal) bypasses 3-point seed plane construction - New Analyze(modelItem, hitPoint, normal) for Navisworks types - Same triangle extraction and centroid computation as 3-point version - Enables single-click end face detection via FaceInferToolPlugin
468 lines
18 KiB
C#
468 lines
18 KiB
C#
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}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 单点+法向量分析端面。用于取面工具得到法向量后替代手工三点拾取。
|
||
/// </summary>
|
||
/// <param name="triangles">模型三角面片</param>
|
||
/// <param name="hitPoint">命中点</param>
|
||
/// <param name="normal">面法向量(需归一化)</param>
|
||
public static EndFaceAnalysisResult Analyze(
|
||
IEnumerable<AnalysisTriangle3> triangles,
|
||
Vector3 hitPoint,
|
||
Vector3 normal)
|
||
{
|
||
if (triangles == null)
|
||
throw new ArgumentNullException(nameof(triangles));
|
||
|
||
if (normal.LengthSquared() < 1e-12f)
|
||
return EndFaceAnalysisResult.Failure("法向量长度过小。");
|
||
|
||
var triangleList = triangles.ToList();
|
||
if (triangleList.Count == 0)
|
||
return EndFaceAnalysisResult.Failure("没有可用的三角几何数据。");
|
||
|
||
var seedPlane = new PlaneDefinition(Vector3.Normalize(normal), Vector3.Dot(normal, hitPoint));
|
||
|
||
var candidateTriangles = new List<AnalysisTriangle3>();
|
||
var candidatePoints = new HashSet<Vector3>(new Vector3EqualityComparer());
|
||
double maxPlaneDeviation = 0.0;
|
||
|
||
foreach (var triangle in triangleList)
|
||
{
|
||
if (!TryGetTriangleNormal(triangle, out Vector3 triangleNormal))
|
||
continue;
|
||
|
||
if (Math.Abs(Vector3.Dot(triangleNormal, seedPlane.Normal)) < NormalAlignmentCosineThreshold)
|
||
continue;
|
||
|
||
double d1 = Math.Abs(GetSignedDistanceToPlane(seedPlane, triangle.Point1));
|
||
double d2 = Math.Abs(GetSignedDistanceToPlane(seedPlane, triangle.Point2));
|
||
double d3 = Math.Abs(GetSignedDistanceToPlane(seedPlane, triangle.Point3));
|
||
double triangleDeviation = Math.Max(d1, Math.Max(d2, d3));
|
||
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}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 单点+法向量分析端面的 Navisworks 兼容重载。
|
||
/// </summary>
|
||
public static EndFaceAnalysisResult Analyze(ModelItem modelItem, Point3D hitPoint, Vector3D normal)
|
||
{
|
||
if (modelItem == null)
|
||
throw new ArgumentNullException(nameof(modelItem));
|
||
|
||
var triangles = GeometryHelper.ExtractTriangles(new[] { modelItem })
|
||
.Select(ConvertTriangle)
|
||
.ToList();
|
||
|
||
return Analyze(triangles, ToVector3(hitPoint), new Vector3((float)normal.X, (float)normal.Y, (float)normal.Z));
|
||
}
|
||
|
||
public static Point3D ToPoint3D(Vector3 point)
|
||
{
|
||
return new Point3D(point.X, point.Y, point.Z);
|
||
}
|
||
|
||
public static Vector3 OrientNormalTowardTarget(Vector3 normal, Vector3 originPoint, Vector3 targetPoint)
|
||
{
|
||
if (normal.LengthSquared() < 1e-12f)
|
||
{
|
||
throw new ArgumentException("端面法向长度过小,无法定向。", nameof(normal));
|
||
}
|
||
|
||
Vector3 normalizedNormal = Vector3.Normalize(normal);
|
||
Vector3 targetDirection = targetPoint - originPoint;
|
||
if (targetDirection.LengthSquared() < 1e-12f)
|
||
{
|
||
return normalizedNormal;
|
||
}
|
||
|
||
Vector3 normalizedTargetDirection = Vector3.Normalize(targetDirection);
|
||
if (Vector3.Dot(normalizedNormal, normalizedTargetDirection) < 0f)
|
||
{
|
||
normalizedNormal = -normalizedNormal;
|
||
}
|
||
|
||
return normalizedNormal;
|
||
}
|
||
|
||
private static AnalysisTriangle3 ConvertTriangle(Triangle3D triangle)
|
||
{
|
||
return new AnalysisTriangle3(ToVector3(triangle.Point1), ToVector3(triangle.Point2), ToVector3(triangle.Point3));
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
}
|