NavisworksTransport/src/Utils/Assembly/AssemblyEndFaceAnalyzer.cs
tian e7b5206198 feat: add single-point+normal overload to AssemblyEndFaceAnalyzer
- 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
2026-06-09 01:51:05 +08:00

468 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}
}