1626 lines
56 KiB
C#
1626 lines
56 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using Autodesk.AutoCAD.DatabaseServices;
|
||
using Autodesk.AutoCAD.Geometry;
|
||
using Autodesk.AutoCAD.BoundaryRepresentation;
|
||
using BrepFace = Autodesk.AutoCAD.BoundaryRepresentation.Face;
|
||
|
||
namespace FurnitureExporter;
|
||
|
||
/// <summary>
|
||
/// 板材分析器 - 识别和提取板材信息
|
||
/// v2.2: 增加板件类型推断
|
||
/// </summary>
|
||
public class PanelAnalyzer
|
||
{
|
||
private readonly Database _db;
|
||
private Point3d _originOffset = Point3d.Origin;
|
||
private Point3d _modelMin = Point3d.Origin;
|
||
private Point3d _modelMax = Point3d.Origin;
|
||
|
||
/// <summary>
|
||
/// 本次 Analyze 过程中被过滤/排除的孔位(兜底/排查模式用)。
|
||
/// Reason 必须为中文,便于设计师理解。
|
||
/// </summary>
|
||
public List<ExcludedHoleInfo> ExcludedHoles { get; } = new();
|
||
|
||
// 板材识别参数
|
||
private const double MIN_PANEL_THICKNESS = 5; // 最小板厚 5mm
|
||
private const double MAX_PANEL_THICKNESS = 50; // 最大板厚 50mm
|
||
private const double MIN_PANEL_SIZE = 50; // 最小板材尺寸 50mm
|
||
|
||
// 板件类型判断参数
|
||
private const double EDGE_TOLERANCE = 50; // 边缘容差 50mm
|
||
|
||
public PanelAnalyzer(Database db)
|
||
{
|
||
_db = db;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据板件尺寸和位置推断板件类型(v2.2 新增)
|
||
/// </summary>
|
||
public static string InferPanelType(PanelInfo panel, Point3d modelMin, Point3d modelMax)
|
||
{
|
||
var size = panel.Size;
|
||
var pos = panel.Position;
|
||
var normal = panel.Normal;
|
||
|
||
var length = size[0];
|
||
var width = size[1];
|
||
var thickness = size[2];
|
||
|
||
var modelWidth = modelMax.X - modelMin.X;
|
||
var modelHeight = modelMax.Z - modelMin.Z;
|
||
var modelDepth = modelMax.Y - modelMin.Y;
|
||
|
||
// 根据法向量判断板件朝向
|
||
var isHorizontal = Math.Abs(normal[2]) > 0.9; // Z 方向法向量 -> 水平板
|
||
var isFrontBack = Math.Abs(normal[1]) > 0.9; // Y 方向法向量 -> 前后板
|
||
var isLeftRight = Math.Abs(normal[0]) > 0.9; // X 方向法向量 -> 左右板
|
||
|
||
// 1. 背板:通常很薄(3-9mm),Y方向法向量,覆盖整个背面
|
||
if (isFrontBack && thickness <= 9 && length > modelWidth * 0.5 && width > modelHeight * 0.3)
|
||
{
|
||
return "back_panel";
|
||
}
|
||
|
||
// 2. 侧板:X方向法向量,高度接近模型高度
|
||
if (isLeftRight && width > modelHeight * 0.7)
|
||
{
|
||
// 判断是否在模型边缘
|
||
var isAtLeftEdge = Math.Abs(pos[0] - (modelMin.X + thickness / 2)) < EDGE_TOLERANCE;
|
||
var isAtRightEdge = Math.Abs(pos[0] - (modelMax.X - thickness / 2)) < EDGE_TOLERANCE;
|
||
|
||
if (isAtLeftEdge || isAtRightEdge)
|
||
return "side_panel";
|
||
}
|
||
|
||
// 3. 顶板/底板:Z方向法向量,在顶部或底部
|
||
if (isHorizontal)
|
||
{
|
||
var isAtTop = Math.Abs(pos[2] - (modelMax.Z - thickness / 2)) < EDGE_TOLERANCE;
|
||
var isAtBottom = Math.Abs(pos[2] - (modelMin.Z + thickness / 2)) < EDGE_TOLERANCE;
|
||
|
||
if (isAtTop && length > modelWidth * 0.7)
|
||
return "top_panel";
|
||
if (isAtBottom && length > modelWidth * 0.7)
|
||
return "bottom_panel";
|
||
}
|
||
|
||
// 4. 层板/隔板:水平方向,在中间区域
|
||
if (isHorizontal)
|
||
{
|
||
return "shelf";
|
||
}
|
||
|
||
// 5. 竖向隔板:X方向法向量,不在边缘
|
||
if (isLeftRight)
|
||
{
|
||
return "vertical_divider";
|
||
}
|
||
|
||
// 6. 横向隔板:Y方向法向量,不是背板
|
||
if (isFrontBack)
|
||
{
|
||
return "horizontal_divider";
|
||
}
|
||
|
||
return "panel"; // 默认类型
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分析板材,提取板材信息和孔位数据
|
||
/// </summary>
|
||
/// <param name="panelIds">板材实体 ID 数组</param>
|
||
/// <param name="originOffset">原点偏移</param>
|
||
/// <returns>板材信息列表</returns>
|
||
public List<PanelInfo> Analyze(ObjectId[] panelIds, Point3d? originOffset = null)
|
||
{
|
||
_originOffset = originOffset ?? Point3d.Origin;
|
||
var panels = new List<PanelInfo>();
|
||
|
||
// 每次分析前清空,避免复用同一个 PanelAnalyzer 实例导致数据串包
|
||
ExcludedHoles.Clear();
|
||
|
||
if (panelIds.Length == 0)
|
||
return panels;
|
||
|
||
using var tr = _db.TransactionManager.StartTransaction();
|
||
|
||
// 先计算所有板材的边界框
|
||
_modelMin = new Point3d(double.MaxValue, double.MaxValue, double.MaxValue);
|
||
_modelMax = new Point3d(double.MinValue, double.MinValue, double.MinValue);
|
||
|
||
foreach (var id in panelIds)
|
||
{
|
||
var ent = tr.GetObject(id, OpenMode.ForRead) as Solid3d;
|
||
if (ent == null) continue;
|
||
|
||
try
|
||
{
|
||
var ext = ent.GeometricExtents;
|
||
_modelMin = new Point3d(
|
||
Math.Min(_modelMin.X, ext.MinPoint.X + _originOffset.X),
|
||
Math.Min(_modelMin.Y, ext.MinPoint.Y + _originOffset.Y),
|
||
Math.Min(_modelMin.Z, ext.MinPoint.Z + _originOffset.Z)
|
||
);
|
||
_modelMax = new Point3d(
|
||
Math.Max(_modelMax.X, ext.MaxPoint.X + _originOffset.X),
|
||
Math.Max(_modelMax.Y, ext.MaxPoint.Y + _originOffset.Y),
|
||
Math.Max(_modelMax.Z, ext.MaxPoint.Z + _originOffset.Z)
|
||
);
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
int panelIndex = 0;
|
||
foreach (var id in panelIds)
|
||
{
|
||
var ent = tr.GetObject(id, OpenMode.ForRead) as Solid3d;
|
||
if (ent == null) continue;
|
||
|
||
var panelInfo = ExtractPanelInfo(ent, panelIndex++);
|
||
if (panelInfo != null)
|
||
{
|
||
panels.Add(panelInfo);
|
||
}
|
||
}
|
||
|
||
tr.Commit();
|
||
|
||
// v2.2: 推断每个板件的类型
|
||
foreach (var panel in panels)
|
||
{
|
||
panel.PanelType = InferPanelType(panel, _modelMin, _modelMax);
|
||
}
|
||
|
||
return panels;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 提取单个板材信息
|
||
/// </summary>
|
||
private PanelInfo? ExtractPanelInfo(Solid3d solid, int index)
|
||
{
|
||
try
|
||
{
|
||
var ext = solid.GeometricExtents;
|
||
var size = ext.MaxPoint - ext.MinPoint;
|
||
var center = ext.MinPoint + (size / 2);
|
||
|
||
// 应用原点偏移
|
||
var offsetCenter = new Point3d(
|
||
center.X + _originOffset.X,
|
||
center.Y + _originOffset.Y,
|
||
center.Z + _originOffset.Z
|
||
);
|
||
|
||
// 获取尺寸(排序后:长、宽、厚)
|
||
var dims = new[] { size.X, size.Y, size.Z }.OrderByDescending(d => d).ToArray();
|
||
|
||
// 验证是否为板材(最小尺寸应该是厚度)
|
||
var thickness = dims[2];
|
||
if (thickness < MIN_PANEL_THICKNESS || thickness > MAX_PANEL_THICKNESS)
|
||
{
|
||
// 可能不是板材,但仍然包含
|
||
}
|
||
|
||
// 判断板材方向(法向量)
|
||
var normal = DeterminePanelNormal(dims, size);
|
||
|
||
var panelBoundingBox = new BoundingBox3D
|
||
{
|
||
Min = new double[]
|
||
{
|
||
Math.Round(ext.MinPoint.X + _originOffset.X, 2),
|
||
Math.Round(ext.MinPoint.Y + _originOffset.Y, 2),
|
||
Math.Round(ext.MinPoint.Z + _originOffset.Z, 2)
|
||
},
|
||
Max = new double[]
|
||
{
|
||
Math.Round(ext.MaxPoint.X + _originOffset.X, 2),
|
||
Math.Round(ext.MaxPoint.Y + _originOffset.Y, 2),
|
||
Math.Round(ext.MaxPoint.Z + _originOffset.Z, 2)
|
||
}
|
||
};
|
||
|
||
// 提取孔位
|
||
var holes = ExtractHoles(solid, $"panel_{index:D3}", thickness, panelBoundingBox);
|
||
|
||
// v2.2.5:孔位过滤(与设计师“孔位口径”对齐)
|
||
// - 极小薄件(如脚垫/垫片)不作为板件孔位统计对象,避免误把“几何细节”当孔
|
||
// - 非板件(厚度不小于宽/高的块状件、柜脚等)上的“大圆柱面/外圆”容易被误判为孔
|
||
// 这里做保守过滤:在非板件上,过滤掉直径很大的“孔”,以及角落处疑似圆角产生的 6mm 圆柱面
|
||
var offsetMin = new Point3d(ext.MinPoint.X + _originOffset.X, ext.MinPoint.Y + _originOffset.Y, ext.MinPoint.Z + _originOffset.Z);
|
||
var offsetMax = new Point3d(ext.MaxPoint.X + _originOffset.X, ext.MaxPoint.Y + _originOffset.Y, ext.MaxPoint.Z + _originOffset.Z);
|
||
|
||
var filterResult = FilterHolesForBusinessMeaning(
|
||
holes,
|
||
dims,
|
||
thickness,
|
||
offsetMin,
|
||
offsetMax
|
||
);
|
||
|
||
if (filterResult.Excluded.Count > 0)
|
||
{
|
||
ExcludedHoles.AddRange(filterResult.Excluded);
|
||
}
|
||
|
||
holes = filterResult.Included;
|
||
|
||
return new PanelInfo
|
||
{
|
||
Id = $"panel_{index:D3}",
|
||
SourceHandle = solid.Handle.ToString(),
|
||
SourceLayer = solid.Layer,
|
||
Position = new double[]
|
||
{
|
||
Math.Round(offsetCenter.X, 2),
|
||
Math.Round(offsetCenter.Y, 2),
|
||
Math.Round(offsetCenter.Z, 2)
|
||
},
|
||
Size = new double[]
|
||
{
|
||
Math.Round(dims[0], 2), // 长
|
||
Math.Round(dims[1], 2), // 宽
|
||
Math.Round(dims[2], 2) // 厚
|
||
},
|
||
Normal = normal,
|
||
BoundingBox = panelBoundingBox,
|
||
Holes = holes
|
||
};
|
||
}
|
||
catch (System.Exception ex)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine($"[PanelAnalyzer] Error extracting panel info: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断板材的法向量方向
|
||
/// </summary>
|
||
private double[] DeterminePanelNormal(double[] sortedDims, Vector3d originalSize)
|
||
{
|
||
// 最小尺寸对应的轴就是板材的厚度方向(法向量方向)
|
||
var thickness = sortedDims[2];
|
||
|
||
if (Math.Abs(originalSize.X - thickness) < 0.1)
|
||
return new double[] { 1, 0, 0 }; // X 方向
|
||
if (Math.Abs(originalSize.Y - thickness) < 0.1)
|
||
return new double[] { 0, 1, 0 }; // Y 方向
|
||
if (Math.Abs(originalSize.Z - thickness) < 0.1)
|
||
return new double[] { 0, 0, 1 }; // Z 方向
|
||
|
||
return new double[] { 0, 0, 1 }; // 默认 Z 方向
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从板材实体中提取孔位信息
|
||
/// </summary>
|
||
private List<HoleInfo> ExtractHoles(
|
||
Solid3d solid,
|
||
string panelId,
|
||
double panelThickness,
|
||
BoundingBox3D panelBoundingBox)
|
||
{
|
||
var holes = new List<HoleInfo>();
|
||
|
||
try
|
||
{
|
||
// 使用 Brep API 分析实体的面
|
||
using var brep = new Brep(solid);
|
||
|
||
int holeIndex = 0;
|
||
foreach (var face in brep.Faces)
|
||
{
|
||
// 注意:face.Surface 常常是 ExternalBoundedSurface(修剪面),需要剥离后才能识别 Cylinder/Plane
|
||
var surface = BrepSurfaceUtils.Unwrap(face.Surface);
|
||
if (surface is Cylinder cylinder)
|
||
{
|
||
var hole = ExtractHoleFromCylinder(cylinder, face, panelId, holeIndex, panelThickness, panelBoundingBox);
|
||
if (hole != null)
|
||
{
|
||
holes.Add(hole);
|
||
holeIndex++;
|
||
}
|
||
}
|
||
else if (surface is Cone cone)
|
||
{
|
||
// 柜脚/三角铁等五金件上常见“沉头孔/倒角孔”,其孔壁在 Brep 中可能表现为锥面(Cone),
|
||
// 仅识别 Cylinder 会漏掉这类孔位。
|
||
var hole = ExtractHoleFromCone(cone, face, panelId, holeIndex, panelThickness, panelBoundingBox);
|
||
if (hole != null)
|
||
{
|
||
holes.Add(hole);
|
||
holeIndex++;
|
||
}
|
||
}
|
||
else if (surface is Plane)
|
||
{
|
||
// 平面不可能是“孔壁面”,跳过
|
||
continue;
|
||
}
|
||
else
|
||
{
|
||
// v2.2.12:兜底识别
|
||
// 某些孔位(尤其是五金件的沉头孔/复杂倒角)在 AutoCAD Brep 中,孔壁面可能被表达为 NurbSurface/其它曲面,
|
||
// 导致 surface is Cylinder/Cone 判断失败。
|
||
// 这里用“闭合圆边界 + 相邻端面平面法向推断孔轴”的方式做一次兜底,尽量补齐漏孔。
|
||
var hole = ExtractHoleFromFallbackFace(face, panelId, holeIndex, panelThickness, panelBoundingBox);
|
||
if (hole != null)
|
||
{
|
||
holes.Add(hole);
|
||
holeIndex++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (System.Exception ex)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine($"[PanelAnalyzer] Error extracting holes: {ex.Message}");
|
||
}
|
||
|
||
// 同一孔位可能由于几何修剪/分段出现重复,做一次合并去重
|
||
return MergeNearbyHoles(holes);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从锥面(沉头/倒角孔)提取孔位信息。
|
||
///
|
||
/// 说明:
|
||
/// - 某些五金件孔位的孔壁不是纯圆柱,而是锥面(例如沉头孔、全倒角导致“圆柱段”长度为 0);
|
||
/// - 这里按“闭合圆边界 + 端面平面邻接”的规则识别,并取闭合圆中最小直径作为孔径(更接近真实通孔直径)。
|
||
/// </summary>
|
||
private HoleInfo? ExtractHoleFromCone(
|
||
Cone cone,
|
||
Autodesk.AutoCAD.BoundaryRepresentation.Face face,
|
||
string panelId,
|
||
int index,
|
||
double panelThickness,
|
||
BoundingBox3D panelBoundingBox)
|
||
{
|
||
try
|
||
{
|
||
// 轴向(孔方向)
|
||
var axis = cone.AxisOfSymmetry;
|
||
if (axis.Length < 1e-9)
|
||
return null;
|
||
axis = axis.GetNormal();
|
||
|
||
// 先用“闭合圆边界”推断孔径(取最小闭合圆半径)
|
||
if (!TryGetMinClosedCircleRadius(face, out var minRadius))
|
||
return null;
|
||
|
||
var diameter = minRadius * 2;
|
||
|
||
// 过滤太大或太小的锥面(可能不是孔)
|
||
if (diameter < 2 || diameter > 100)
|
||
return null;
|
||
|
||
// 同样复用“像孔”的判定(闭合圆 + 端面平面法向与轴向平行)
|
||
if (!LooksLikeHoleCylinderFace(face, axis))
|
||
return null;
|
||
|
||
var hasCenterAndDepth = TryGetHoleCenterAndDepth(face, axis, out var holeCenter, out var holeDepth);
|
||
var origin = hasCenterAndDepth ? holeCenter : cone.BaseCenter;
|
||
|
||
// 应用原点偏移
|
||
var offsetPosition = new Point3d(
|
||
origin.X + _originOffset.X,
|
||
origin.Y + _originOffset.Y,
|
||
origin.Z + _originOffset.Z
|
||
);
|
||
|
||
// 深度:优先使用边界圆推断值;否则用锥面的 Height;再兜底
|
||
double depth = holeDepth;
|
||
if (depth <= 0.01)
|
||
{
|
||
try
|
||
{
|
||
if (!cone.Height.IsUnbounded && cone.Height.Length > 0)
|
||
{
|
||
depth = cone.Height.Length;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
}
|
||
if (depth <= 0.01)
|
||
{
|
||
depth = panelThickness > 0 ? panelThickness : 18.0;
|
||
}
|
||
|
||
// 判断孔类型(通孔/盲孔):与圆柱孔保持一致的简化策略
|
||
string holeType = "blind";
|
||
if (panelThickness > 0 && Math.Abs(depth - panelThickness) <= 2.0)
|
||
{
|
||
holeType = "through";
|
||
}
|
||
|
||
return new HoleInfo
|
||
{
|
||
Id = $"{panelId}_hole_{index:D3}",
|
||
Position = new double[]
|
||
{
|
||
Math.Round(offsetPosition.X, 2),
|
||
Math.Round(offsetPosition.Y, 2),
|
||
Math.Round(offsetPosition.Z, 2)
|
||
},
|
||
Normal = new double[]
|
||
{
|
||
Math.Round(axis.X, 4),
|
||
Math.Round(axis.Y, 4),
|
||
Math.Round(axis.Z, 4)
|
||
},
|
||
Diameter = Math.Round(diameter, 2),
|
||
Depth = Math.Round(depth, 2),
|
||
PanelId = panelId,
|
||
Type = holeType,
|
||
DetectionSource = "brep",
|
||
BindingSource = "panel_brep",
|
||
BindingConfidence = 1.0,
|
||
BindingReason = "从当前板件实体 Brep 直接提取",
|
||
SurfaceFaces = PanelFaceSemanticResolver.ResolveSurfaceFaces(
|
||
panelBoundingBox,
|
||
new[] { offsetPosition.X, offsetPosition.Y, offsetPosition.Z },
|
||
new[] { axis.X, axis.Y, axis.Z },
|
||
depth)
|
||
};
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从圆柱面提取孔位信息
|
||
/// </summary>
|
||
private HoleInfo? ExtractHoleFromCylinder(
|
||
Cylinder cylinder,
|
||
Autodesk.AutoCAD.BoundaryRepresentation.Face face,
|
||
string panelId,
|
||
int index,
|
||
double panelThickness,
|
||
BoundingBox3D panelBoundingBox)
|
||
{
|
||
try
|
||
{
|
||
var radius = cylinder.Radius;
|
||
var diameter = radius * 2;
|
||
|
||
// 过滤太大或太小的圆柱(可能不是孔)
|
||
if (diameter < 2 || diameter > 100)
|
||
return null;
|
||
|
||
// 获取圆柱轴向(孔的方向)
|
||
var axis = cylinder.AxisOfSymmetry;
|
||
if (axis.Length < 1e-9)
|
||
return null;
|
||
axis = axis.GetNormal();
|
||
|
||
// 过滤:圆柱面很多时候是圆角/外圆,孔位通常会与“端面平面”相邻且平面法向量与轴向平行
|
||
if (!LooksLikeHoleCylinderFace(face, axis))
|
||
return null;
|
||
|
||
// 尝试从圆柱面的边界圆获取孔中心与深度(比 cylinder.Origin 更稳定)
|
||
var hasCenterAndDepth = TryGetHoleCenterAndDepth(face, axis, out var holeCenter, out var holeDepth);
|
||
|
||
// 获取孔的位置(优先用边界圆推断的中心,否则用圆柱原点兜底)
|
||
var origin = hasCenterAndDepth ? holeCenter : cylinder.Origin;
|
||
|
||
// 应用原点偏移
|
||
var offsetPosition = new Point3d(
|
||
origin.X + _originOffset.X,
|
||
origin.Y + _originOffset.Y,
|
||
origin.Z + _originOffset.Z
|
||
);
|
||
|
||
// 深度:优先使用边界圆推断值;如果失败则用面自身 Height 或板厚兜底
|
||
double depth = holeDepth;
|
||
if (depth <= 0.01)
|
||
{
|
||
try
|
||
{
|
||
if (!cylinder.Height.IsUnbounded && cylinder.Height.Length > 0)
|
||
{
|
||
depth = cylinder.Height.Length;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
}
|
||
if (depth <= 0.01)
|
||
{
|
||
depth = panelThickness > 0 ? panelThickness : 18.0;
|
||
}
|
||
|
||
// 判断孔类型(通孔/盲孔)
|
||
// 简化策略:深度接近板厚 => 通孔,否则盲孔(容差 2mm)
|
||
string holeType = "blind";
|
||
if (panelThickness > 0 && Math.Abs(depth - panelThickness) <= 2.0)
|
||
{
|
||
holeType = "through";
|
||
}
|
||
|
||
return new HoleInfo
|
||
{
|
||
Id = $"{panelId}_hole_{index:D3}",
|
||
Position = new double[]
|
||
{
|
||
Math.Round(offsetPosition.X, 2),
|
||
Math.Round(offsetPosition.Y, 2),
|
||
Math.Round(offsetPosition.Z, 2)
|
||
},
|
||
Normal = new double[]
|
||
{
|
||
Math.Round(axis.X, 4),
|
||
Math.Round(axis.Y, 4),
|
||
Math.Round(axis.Z, 4)
|
||
},
|
||
Diameter = Math.Round(diameter, 2),
|
||
Depth = Math.Round(depth, 2),
|
||
PanelId = panelId,
|
||
Type = holeType,
|
||
DetectionSource = "brep",
|
||
BindingSource = "panel_brep",
|
||
BindingConfidence = 1.0,
|
||
BindingReason = "从当前板件实体 Brep 直接提取",
|
||
SurfaceFaces = PanelFaceSemanticResolver.ResolveSurfaceFaces(
|
||
panelBoundingBox,
|
||
new[] { offsetPosition.X, offsetPosition.Y, offsetPosition.Z },
|
||
new[] { axis.X, axis.Y, axis.Z },
|
||
depth)
|
||
};
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 兜底孔位识别:当孔壁面不是 Cylinder/Cone(例如被表达为 NURBS/旋转曲面)时,
|
||
/// 通过“闭合圆边界 + 相邻端面平面”推断孔轴/孔径/中心。
|
||
/// </summary>
|
||
private HoleInfo? ExtractHoleFromFallbackFace(
|
||
BrepFace face,
|
||
string panelId,
|
||
int index,
|
||
double panelThickness,
|
||
BoundingBox3D panelBoundingBox)
|
||
{
|
||
try
|
||
{
|
||
// 必须存在闭合圆边界,否则不计为孔(避免把开槽端部/造型曲面误判为孔)
|
||
if (!HasClosedCircularBoundary(face))
|
||
return null;
|
||
|
||
// 从相邻端面平面推断孔轴
|
||
if (!TryInferHoleAxisFromAdjacentPlanes(face, out var axis))
|
||
return null;
|
||
|
||
axis = axis.Length > 1e-9 ? axis.GetNormal() : axis;
|
||
if (axis.Length < 1e-9)
|
||
return null;
|
||
|
||
// 用闭合圆推断孔径(取最小闭合圆半径,尽量贴近真实通孔直径)
|
||
if (!TryGetMinClosedCircleRadius(face, out var minRadius))
|
||
return null;
|
||
|
||
var diameter = minRadius * 2;
|
||
if (diameter < 2 || diameter > 100)
|
||
return null;
|
||
|
||
// 复用“像孔”的判定:端面平面法向与轴向平行(避免外圆/圆角被当孔)
|
||
if (!LooksLikeHoleCylinderFace(face, axis))
|
||
return null;
|
||
|
||
if (!TryGetHoleCenterAndDepth(face, axis, out var holeCenter, out var holeDepth))
|
||
return null;
|
||
|
||
// 应用原点偏移
|
||
var offsetPosition = new Point3d(
|
||
holeCenter.X + _originOffset.X,
|
||
holeCenter.Y + _originOffset.Y,
|
||
holeCenter.Z + _originOffset.Z
|
||
);
|
||
|
||
// 深度兜底:无法从曲面可靠拿到高度时,用板厚/默认值
|
||
var depth = holeDepth;
|
||
if (depth <= 0.01)
|
||
{
|
||
depth = panelThickness > 0 ? panelThickness : 18.0;
|
||
}
|
||
|
||
// 判断孔类型(通孔/盲孔):与圆柱孔保持一致的简化策略
|
||
string holeType = "blind";
|
||
if (panelThickness > 0 && Math.Abs(depth - panelThickness) <= 2.0)
|
||
{
|
||
holeType = "through";
|
||
}
|
||
|
||
return new HoleInfo
|
||
{
|
||
Id = $"{panelId}_hole_{index:D3}",
|
||
Position = new double[]
|
||
{
|
||
Math.Round(offsetPosition.X, 2),
|
||
Math.Round(offsetPosition.Y, 2),
|
||
Math.Round(offsetPosition.Z, 2)
|
||
},
|
||
Normal = new double[]
|
||
{
|
||
Math.Round(axis.X, 4),
|
||
Math.Round(axis.Y, 4),
|
||
Math.Round(axis.Z, 4)
|
||
},
|
||
Diameter = Math.Round(diameter, 2),
|
||
Depth = Math.Round(depth, 2),
|
||
PanelId = panelId,
|
||
Type = holeType,
|
||
DetectionSource = "brep",
|
||
BindingSource = "panel_brep",
|
||
BindingConfidence = 1.0,
|
||
BindingReason = "从当前板件实体 Brep 兜底面直接提取",
|
||
SurfaceFaces = PanelFaceSemanticResolver.ResolveSurfaceFaces(
|
||
panelBoundingBox,
|
||
new[] { offsetPosition.X, offsetPosition.Y, offsetPosition.Z },
|
||
new[] { axis.X, axis.Y, axis.Z },
|
||
depth)
|
||
};
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private static bool TryInferHoleAxisFromAdjacentPlanes(BrepFace face, out Vector3d axis)
|
||
{
|
||
axis = Vector3d.ZAxis;
|
||
|
||
try
|
||
{
|
||
var normals = new List<Vector3d>();
|
||
var seenFaces = new HashSet<int>();
|
||
|
||
foreach (var loop in face.Loops)
|
||
{
|
||
foreach (var edge in loop.Edges)
|
||
{
|
||
foreach (var edgeLoop in edge.Loops)
|
||
{
|
||
var otherFace = edgeLoop.Face;
|
||
if (otherFace == null)
|
||
continue;
|
||
if (ReferenceEquals(otherFace, face))
|
||
continue;
|
||
if (!seenFaces.Add(otherFace.GetHashCode()))
|
||
continue;
|
||
|
||
var otherSurface = BrepSurfaceUtils.Unwrap(otherFace.Surface);
|
||
if (otherSurface is Plane plane)
|
||
{
|
||
var n = plane.Normal;
|
||
if (n.Length < 1e-9)
|
||
continue;
|
||
normals.Add(n.GetNormal());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (normals.Count == 0)
|
||
return false;
|
||
|
||
// 聚类:把近似平行/反平行的法向归为一组(孔通常会有 1~2 个端面平面,法向与孔轴一致)
|
||
const double clusterDot = 0.98;
|
||
var clusters = new List<(Vector3d Dir, int Count)>();
|
||
|
||
foreach (var n in normals)
|
||
{
|
||
var added = false;
|
||
for (int i = 0; i < clusters.Count; i++)
|
||
{
|
||
var c = clusters[i];
|
||
var dot = Math.Abs(c.Dir.DotProduct(n));
|
||
if (dot >= clusterDot)
|
||
{
|
||
clusters[i] = (c.Dir, c.Count + 1);
|
||
added = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!added)
|
||
{
|
||
clusters.Add((n, 1));
|
||
}
|
||
}
|
||
|
||
// 选出现次数最多的方向作为孔轴
|
||
var best = clusters.OrderByDescending(c => c.Count).First();
|
||
axis = best.Dir.Length > 1e-9 ? best.Dir.GetNormal() : best.Dir;
|
||
return axis.Length >= 1e-9;
|
||
}
|
||
catch
|
||
{
|
||
axis = Vector3d.ZAxis;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按“业务孔位口径”过滤孔位。
|
||
///
|
||
/// 说明:当前阶段我们优先保证:
|
||
/// - 导出孔位数量与设计师数孔一致(避免 Web 端对不上)
|
||
/// - 过滤规则仅对“非板件/极小薄件”生效,尽量不影响常规板件孔位
|
||
/// </summary>
|
||
private sealed class HoleFilterResult
|
||
{
|
||
public HoleFilterResult(List<HoleInfo> included, List<ExcludedHoleInfo> excluded)
|
||
{
|
||
Included = included;
|
||
Excluded = excluded;
|
||
}
|
||
|
||
public List<HoleInfo> Included { get; }
|
||
public List<ExcludedHoleInfo> Excluded { get; }
|
||
}
|
||
|
||
private static HoleFilterResult FilterHolesForBusinessMeaning(
|
||
List<HoleInfo> holes,
|
||
double[] sortedDims,
|
||
double thickness,
|
||
Point3d bboxMin,
|
||
Point3d bboxMax)
|
||
{
|
||
if (holes.Count == 0)
|
||
return new HoleFilterResult(holes, new List<ExcludedHoleInfo>());
|
||
|
||
// 极小薄件:例如 8x8x1.2 的脚垫/垫片
|
||
// 这类零件在安装展示里通常作为“五金/配件”,不参与板件孔位统计。
|
||
var isTinyPart = sortedDims[0] < MIN_PANEL_SIZE && sortedDims[1] < MIN_PANEL_SIZE && thickness < MIN_PANEL_THICKNESS;
|
||
if (isTinyPart)
|
||
{
|
||
var excludedTiny = holes
|
||
.Select(h => new ExcludedHoleInfo
|
||
{
|
||
Hole = h,
|
||
Reason = "极小薄件(例如垫片/脚垫):作为配件不参与孔位统计"
|
||
})
|
||
.ToList();
|
||
return new HoleFilterResult(new List<HoleInfo>(), excludedTiny);
|
||
}
|
||
|
||
// 识别“板件”:厚度显著小于宽/高,且尺寸不太小
|
||
var isPanelLike =
|
||
thickness <= MAX_PANEL_THICKNESS &&
|
||
sortedDims[0] >= MIN_PANEL_SIZE &&
|
||
sortedDims[1] >= MIN_PANEL_SIZE &&
|
||
sortedDims[1] / Math.Max(thickness, 0.0001) >= 4.0;
|
||
|
||
if (isPanelLike)
|
||
{
|
||
// 常规板件不过滤,避免误删真实孔(包括过线孔等大孔)
|
||
return new HoleFilterResult(holes, new List<ExcludedHoleInfo>());
|
||
}
|
||
|
||
var included = new List<HoleInfo>();
|
||
var excluded = new List<ExcludedHoleInfo>();
|
||
|
||
// 非板件过滤参数(基于当前样例归纳,后续可继续数据驱动)
|
||
const double largeHoleDiameterMm = 20.0;
|
||
const double cornerTolMm = 10.0;
|
||
const double zFaceTolMm = 2.5;
|
||
|
||
foreach (var h in holes)
|
||
{
|
||
// 1) 非板件上的“大孔”(>=20mm)极易来自外圆/曲面,默认不计入孔位
|
||
if (h.Diameter >= largeHoleDiameterMm)
|
||
{
|
||
excluded.Add(new ExcludedHoleInfo
|
||
{
|
||
Hole = h,
|
||
Reason = "非板件:直径较大的圆柱面(>=20mm)容易是外圆/造型曲面误判,默认排除"
|
||
});
|
||
continue;
|
||
}
|
||
|
||
// 2) 角落处的 6mm 圆柱面,常见于圆角/造型面误判为孔
|
||
// 经验特征:靠近 XY 角、靠近顶部或底部面,且轴向接近 Z(与样例一致)
|
||
if (h.Diameter >= 5.5 && h.Diameter <= 6.5 && IsAxisMostlyZ(h.Normal))
|
||
{
|
||
var x = h.Position[0];
|
||
var y = h.Position[1];
|
||
var z = h.Position[2];
|
||
|
||
var nearX = Math.Abs(x - bboxMin.X) <= cornerTolMm || Math.Abs(x - bboxMax.X) <= cornerTolMm;
|
||
var nearY = Math.Abs(y - bboxMin.Y) <= cornerTolMm || Math.Abs(y - bboxMax.Y) <= cornerTolMm;
|
||
var nearCornerXY = nearX && nearY;
|
||
|
||
var nearTopOrBottom = Math.Abs(z - bboxMin.Z) <= zFaceTolMm || Math.Abs(z - bboxMax.Z) <= zFaceTolMm;
|
||
|
||
if (nearCornerXY && nearTopOrBottom)
|
||
{
|
||
excluded.Add(new ExcludedHoleInfo
|
||
{
|
||
Hole = h,
|
||
Reason = "非板件:角落附近且贴近顶/底面的 6mm 圆柱面,常见为圆角/造型面误判,默认排除"
|
||
});
|
||
continue;
|
||
}
|
||
}
|
||
|
||
included.Add(h);
|
||
}
|
||
|
||
return new HoleFilterResult(included, excluded);
|
||
}
|
||
|
||
private static bool IsAxisMostlyZ(double[] normal)
|
||
{
|
||
if (normal.Length < 3)
|
||
return false;
|
||
return Math.Abs(normal[2]) >= 0.9;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断一个圆柱面更像“打孔”而不是“圆角/外圆”。
|
||
///
|
||
/// 经验规则:孔位的圆柱面通常会与至少一个端面平面相邻,且该平面的法向量与圆柱轴向平行。
|
||
/// 圆角则相反:它连接两块相交的平面,平面法向量通常与轴向近似垂直。
|
||
/// </summary>
|
||
private static bool LooksLikeHoleCylinderFace(BrepFace cylinderFace, Vector3d axis)
|
||
{
|
||
// v2.2.6:孔位必须是“闭合圆形”边界(完整 360 度),否则不计为孔。
|
||
// 目的:避免把开槽端部、局部圆弧、造型圆角等误判为孔位。
|
||
if (!HasClosedCircularBoundary(cylinderFace))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const double dotThreshold = 0.90;
|
||
var axisN = axis.Length > 1e-9 ? axis.GetNormal() : axis;
|
||
|
||
var seenFaces = new HashSet<int>();
|
||
int parallelPlaneCount = 0;
|
||
|
||
foreach (var loop in cylinderFace.Loops)
|
||
{
|
||
foreach (var edge in loop.Edges)
|
||
{
|
||
foreach (var edgeLoop in edge.Loops)
|
||
{
|
||
var otherFace = edgeLoop.Face;
|
||
if (otherFace == null)
|
||
continue;
|
||
|
||
// 跳过自身
|
||
if (ReferenceEquals(otherFace, cylinderFace))
|
||
continue;
|
||
|
||
if (!seenFaces.Add(otherFace.GetHashCode()))
|
||
continue;
|
||
|
||
var otherSurface = BrepSurfaceUtils.Unwrap(otherFace.Surface);
|
||
if (otherSurface is Plane plane)
|
||
{
|
||
var n = plane.Normal;
|
||
if (n.Length < 1e-9)
|
||
continue;
|
||
n = n.GetNormal();
|
||
|
||
var dot = Math.Abs(axisN.DotProduct(n));
|
||
if (dot >= dotThreshold)
|
||
{
|
||
parallelPlaneCount++;
|
||
if (parallelPlaneCount >= 1)
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断圆柱面的边界是否存在“闭合圆”(完整 360 度)。
|
||
///
|
||
/// 说明:
|
||
/// - 真正的圆孔/钻孔,圆柱面的端口边界通常是完整圆;
|
||
/// - 开槽的端部、圆角等往往只有局部圆弧,不应被计为孔位。
|
||
/// </summary>
|
||
private static bool HasClosedCircularBoundary(BrepFace cylinderFace)
|
||
{
|
||
const double centerTol = 0.2; // mm:容忍 CAD 内部修剪/分段带来的数值抖动
|
||
const double radiusTol = 0.2; // mm
|
||
const double angleTol = 0.20; // rad(约 11 度),用于容忍弧段分割误差
|
||
var full = 2.0 * Math.PI;
|
||
|
||
foreach (var loop in cylinderFace.Loops)
|
||
{
|
||
// 注意:BrepEdge.Curve 常见是 ExternalCurve3d(包装类型),不能直接 as CircularArc3d。
|
||
// 我们统一做一次“解包”,并支持 CompositeCurve3d(由多个弧段组成)。
|
||
var segs = new List<CircleArcSegment>();
|
||
foreach (var edge in loop.Edges)
|
||
{
|
||
foreach (var seg in EnumerateCircleArcSegments(edge.Curve, full, radiusTol))
|
||
segs.Add(seg);
|
||
}
|
||
|
||
if (segs.Count == 0)
|
||
continue;
|
||
|
||
// 同一个闭合圆可能由多个弧段组成;而一个圆柱面也可能同时包含“上下两个端口圆”。
|
||
// 所以这里把弧段按“近似同心同半径”分组,只要任意一组覆盖 360 度,就视为存在闭合圆。
|
||
var groups = new List<(Point3d Center, double Radius, double SweepSum)>();
|
||
foreach (var seg in segs)
|
||
{
|
||
var c = seg.Center;
|
||
var r = seg.Radius;
|
||
var sweep = seg.SweepAngle;
|
||
|
||
var added = false;
|
||
for (int i = 0; i < groups.Count; i++)
|
||
{
|
||
var g = groups[i];
|
||
if (g.Center.DistanceTo(c) <= centerTol && Math.Abs(g.Radius - r) <= radiusTol)
|
||
{
|
||
groups[i] = (g.Center, g.Radius, g.SweepSum + sweep);
|
||
added = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!added)
|
||
{
|
||
groups.Add((c, r, sweep));
|
||
}
|
||
}
|
||
|
||
foreach (var g in groups)
|
||
{
|
||
// 允许轻微重叠/分段误差:覆盖接近 360 度即可
|
||
if (g.SweepSum >= full - angleTol)
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从一个面边界中找出“闭合圆”的最小半径。
|
||
///
|
||
/// 用途:
|
||
/// - 沉头孔/倒角孔的孔壁可能是锥面(Cone),无法从 Cylinder.Radius 获取“通孔直径”;
|
||
/// - 此时通过边界的闭合圆(通常有两个端口圆)推断孔径,取最小值更接近真实孔径。
|
||
/// </summary>
|
||
private static bool TryGetMinClosedCircleRadius(BrepFace face, out double minRadius)
|
||
{
|
||
minRadius = 0;
|
||
|
||
const double centerTol = 0.2; // mm
|
||
const double radiusTol = 0.2; // mm
|
||
const double angleTol = 0.20; // rad
|
||
var full = 2.0 * Math.PI;
|
||
|
||
var closedRadii = new List<double>();
|
||
|
||
foreach (var loop in face.Loops)
|
||
{
|
||
var segs = new List<CircleArcSegment>();
|
||
foreach (var edge in loop.Edges)
|
||
{
|
||
foreach (var seg in EnumerateCircleArcSegments(edge.Curve, full, radiusTol))
|
||
segs.Add(seg);
|
||
}
|
||
|
||
if (segs.Count == 0)
|
||
continue;
|
||
|
||
var groups = new List<(Point3d Center, double Radius, double SweepSum)>();
|
||
foreach (var seg in segs)
|
||
{
|
||
var c = seg.Center;
|
||
var r = seg.Radius;
|
||
var sweep = seg.SweepAngle;
|
||
|
||
var added = false;
|
||
for (int i = 0; i < groups.Count; i++)
|
||
{
|
||
var g = groups[i];
|
||
if (g.Center.DistanceTo(c) <= centerTol && Math.Abs(g.Radius - r) <= radiusTol)
|
||
{
|
||
groups[i] = (g.Center, g.Radius, g.SweepSum + sweep);
|
||
added = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!added)
|
||
{
|
||
groups.Add((c, r, sweep));
|
||
}
|
||
}
|
||
|
||
foreach (var g in groups)
|
||
{
|
||
if (g.SweepSum >= full - angleTol)
|
||
closedRadii.Add(g.Radius);
|
||
}
|
||
}
|
||
|
||
if (closedRadii.Count == 0)
|
||
return false;
|
||
|
||
minRadius = closedRadii.Min();
|
||
return minRadius > 0.0001;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 圆弧段(用于“闭合圆”判断)
|
||
/// 只保留:中心、半径、弧长角(用于累计判断是否覆盖 360 度)。
|
||
/// </summary>
|
||
private readonly struct CircleArcSegment
|
||
{
|
||
public Point3d Center { get; }
|
||
public double Radius { get; }
|
||
public double SweepAngle { get; }
|
||
|
||
public CircleArcSegment(Point3d center, double radius, double sweepAngle)
|
||
{
|
||
Center = center;
|
||
Radius = radius;
|
||
SweepAngle = sweepAngle;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从 BrepEdge.Curve 中提取“圆弧段”。
|
||
///
|
||
/// 说明:
|
||
/// - AutoCAD 的 Brep API 常返回 ExternalCurve3d(包装 curve),需要取 NativeCurve;
|
||
/// - 某些边界可能是 CompositeCurve3d(由多个子曲线组成)。
|
||
/// </summary>
|
||
private static IEnumerable<CircleArcSegment> EnumerateCircleArcSegments(Curve3d? curve, double full, double radiusTol)
|
||
{
|
||
if (curve == null)
|
||
yield break;
|
||
|
||
if (curve is CircularArc3d direct)
|
||
{
|
||
yield return new CircleArcSegment(direct.Center, direct.Radius, GetArcSweepAngle(direct, full));
|
||
yield break;
|
||
}
|
||
|
||
// EllipticalArc3d:很多情况下“圆”会以椭圆弧形式返回(但 Major/Minor 半径相等)
|
||
if (curve is EllipticalArc3d eArc)
|
||
{
|
||
if (LooksLikeCircularEllipse(eArc, radiusTol))
|
||
{
|
||
var r = (eArc.MajorRadius + eArc.MinorRadius) / 2.0;
|
||
yield return new CircleArcSegment(eArc.Center, r, GetArcSweepAngle(eArc, full));
|
||
}
|
||
yield break;
|
||
}
|
||
|
||
// ExternalCurve3d:优先解包到 NativeCurve 再判断
|
||
if (curve is ExternalCurve3d external)
|
||
{
|
||
Curve3d? native = null;
|
||
try
|
||
{
|
||
native = external.NativeCurve;
|
||
}
|
||
catch
|
||
{
|
||
native = null;
|
||
}
|
||
|
||
if (native != null && !ReferenceEquals(native, curve))
|
||
{
|
||
foreach (var seg in EnumerateCircleArcSegments(native, full, radiusTol))
|
||
yield return seg;
|
||
}
|
||
|
||
yield break;
|
||
}
|
||
|
||
// CompositeCurve3d:递归枚举子曲线
|
||
if (curve is CompositeCurve3d composite)
|
||
{
|
||
Curve3d[]? curves = null;
|
||
try
|
||
{
|
||
curves = composite.GetCurves();
|
||
}
|
||
catch
|
||
{
|
||
curves = null;
|
||
}
|
||
|
||
if (curves != null)
|
||
{
|
||
foreach (var c in curves)
|
||
{
|
||
foreach (var seg in EnumerateCircleArcSegments(c, full, radiusTol))
|
||
yield return seg;
|
||
}
|
||
}
|
||
|
||
yield break;
|
||
}
|
||
|
||
// Nurb/Polyline/Spline:某些孔位边界会以 NURBS/样条曲线形式返回(几何上仍是圆)。
|
||
// 为避免漏孔,这里做一次“轻量拟合”识别:采样若干点 -> 拟合圆心/半径 -> 校验误差。
|
||
if (curve is NurbCurve3d nurb)
|
||
{
|
||
if (TryApproximateCircleArcSegment(nurb, full, radiusTol, out var seg))
|
||
yield return seg;
|
||
yield break;
|
||
}
|
||
|
||
if (curve is PolylineCurve3d polyline)
|
||
{
|
||
if (TryApproximateCircleArcSegment(polyline, full, radiusTol, out var seg))
|
||
yield return seg;
|
||
yield break;
|
||
}
|
||
|
||
if (curve is CubicSplineCurve3d spline)
|
||
{
|
||
if (TryApproximateCircleArcSegment(spline, full, radiusTol, out var seg))
|
||
yield return seg;
|
||
yield break;
|
||
}
|
||
|
||
// 其它曲线类型(线段、椭圆、偏移曲线等)不计入“闭合圆”判断
|
||
}
|
||
|
||
private static bool TryApproximateCircleArcSegment(
|
||
NurbCurve3d curve,
|
||
double full,
|
||
double radiusTol,
|
||
out CircleArcSegment segment)
|
||
{
|
||
return TryApproximateCircleArcSegmentInternal(
|
||
curve,
|
||
curve.StartParameter,
|
||
curve.EndParameter,
|
||
curve.HasStartPoint ? curve.StartPoint : (Point3d?)null,
|
||
curve.HasEndPoint ? curve.EndPoint : (Point3d?)null,
|
||
full,
|
||
radiusTol,
|
||
out segment);
|
||
}
|
||
|
||
private static bool TryApproximateCircleArcSegment(
|
||
PolylineCurve3d curve,
|
||
double full,
|
||
double radiusTol,
|
||
out CircleArcSegment segment)
|
||
{
|
||
return TryApproximateCircleArcSegmentInternal(
|
||
curve,
|
||
curve.StartParameter,
|
||
curve.EndParameter,
|
||
curve.HasStartPoint ? curve.StartPoint : (Point3d?)null,
|
||
curve.HasEndPoint ? curve.EndPoint : (Point3d?)null,
|
||
full,
|
||
radiusTol,
|
||
out segment);
|
||
}
|
||
|
||
private static bool TryApproximateCircleArcSegment(
|
||
CubicSplineCurve3d curve,
|
||
double full,
|
||
double radiusTol,
|
||
out CircleArcSegment segment)
|
||
{
|
||
return TryApproximateCircleArcSegmentInternal(
|
||
curve,
|
||
curve.StartParameter,
|
||
curve.EndParameter,
|
||
curve.HasStartPoint ? curve.StartPoint : (Point3d?)null,
|
||
curve.HasEndPoint ? curve.EndPoint : (Point3d?)null,
|
||
full,
|
||
radiusTol,
|
||
out segment);
|
||
}
|
||
|
||
private static bool TryApproximateCircleArcSegmentInternal(
|
||
Curve3d curve,
|
||
double startParam,
|
||
double endParam,
|
||
Point3d? startPoint,
|
||
Point3d? endPoint,
|
||
double full,
|
||
double radiusTol,
|
||
out CircleArcSegment segment)
|
||
{
|
||
segment = default;
|
||
|
||
try
|
||
{
|
||
if (endParam <= startParam + 1e-9)
|
||
return false;
|
||
|
||
// 采样 3 点拟合圆
|
||
var p0 = TryEvalPointOnCurve(curve, startParam);
|
||
var p1 = TryEvalPointOnCurve(curve, startParam + (endParam - startParam) * 0.33);
|
||
var p2 = TryEvalPointOnCurve(curve, startParam + (endParam - startParam) * 0.66);
|
||
|
||
if (p0 == null || p1 == null || p2 == null)
|
||
return false;
|
||
|
||
if (!TryCircumcenter3D(p0.Value, p1.Value, p2.Value, out var center, out var planeNormal))
|
||
return false;
|
||
|
||
var radius = center.DistanceTo(p0.Value);
|
||
if (radius <= 0.0001)
|
||
return false;
|
||
|
||
// 校验:采样更多点,检查是否同心同半径 + 近似共面
|
||
const int sampleCount = 10;
|
||
var angles = new double[sampleCount];
|
||
Point3d a = p0.Value;
|
||
|
||
// 在拟合平面内构造正交基(u,v)
|
||
var u = (p1.Value - p0.Value);
|
||
if (u.Length < 1e-6)
|
||
return false;
|
||
u = u.GetNormal();
|
||
var n = planeNormal;
|
||
if (n.Length < 1e-6)
|
||
return false;
|
||
n = n.GetNormal();
|
||
var v = n.CrossProduct(u);
|
||
if (v.Length < 1e-6)
|
||
return false;
|
||
v = v.GetNormal();
|
||
|
||
for (int i = 0; i < sampleCount; i++)
|
||
{
|
||
var t = startParam + (endParam - startParam) * i / (sampleCount - 1.0);
|
||
var pi = TryEvalPointOnCurve(curve, t);
|
||
if (pi == null)
|
||
return false;
|
||
|
||
// 平面偏离(避免把空间曲线误当圆)
|
||
var planeDist = Math.Abs((pi.Value - a).DotProduct(n));
|
||
if (planeDist > Math.Max(0.25, radiusTol * 1.5))
|
||
return false;
|
||
|
||
var dist = center.DistanceTo(pi.Value);
|
||
if (Math.Abs(dist - radius) > Math.Max(0.25, radiusTol * 1.5))
|
||
return false;
|
||
|
||
var rel = pi.Value - center;
|
||
var x = rel.DotProduct(u);
|
||
var y = rel.DotProduct(v);
|
||
angles[i] = Math.Atan2(y, x);
|
||
}
|
||
|
||
// sweep:用角度展开后的首尾差值估计(避免方向/象限跳变)
|
||
var sweep = EstimateSweepFromAngles(angles);
|
||
|
||
// 闭合圆常见:start/end 点重合,sweep 可能被估计为 0,这里直接视为整圆
|
||
if (startPoint.HasValue && endPoint.HasValue && startPoint.Value.DistanceTo(endPoint.Value) <= 0.1)
|
||
{
|
||
sweep = full;
|
||
}
|
||
|
||
if (sweep <= 1e-3)
|
||
return false;
|
||
|
||
segment = new CircleArcSegment(center, radius, sweep);
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static Point3d? TryEvalPointOnCurve(Curve3d curve, double param)
|
||
{
|
||
try
|
||
{
|
||
using var p = new PointOnCurve3d(curve, param);
|
||
return p.Point;
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private static bool TryCircumcenter3D(Point3d a, Point3d b, Point3d c, out Point3d center, out Vector3d planeNormal)
|
||
{
|
||
center = Point3d.Origin;
|
||
planeNormal = Vector3d.ZAxis;
|
||
|
||
var ab = b - a;
|
||
var ac = c - a;
|
||
var cross = ab.CrossProduct(ac);
|
||
if (cross.Length < 1e-6)
|
||
return false;
|
||
|
||
planeNormal = cross.GetNormal();
|
||
|
||
// 构造平面内基向量 u,v(u 取 AB 方向)
|
||
var u = ab;
|
||
if (u.Length < 1e-6)
|
||
return false;
|
||
var uN = u.GetNormal();
|
||
var vN = planeNormal.CrossProduct(uN);
|
||
if (vN.Length < 1e-6)
|
||
return false;
|
||
vN = vN.GetNormal();
|
||
|
||
// 转到 2D:A=(0,0), B=(bx,0), C=(cx,cy)
|
||
var bx = ab.Length;
|
||
var cx = ac.DotProduct(uN);
|
||
var cy = ac.DotProduct(vN);
|
||
if (Math.Abs(cy) < 1e-8)
|
||
return false;
|
||
|
||
var ux = bx / 2.0;
|
||
var uy = (cx * cx + cy * cy - bx * cx) / (2.0 * cy);
|
||
|
||
center = a + (uN * ux) + (vN * uy);
|
||
return true;
|
||
}
|
||
|
||
private static double EstimateSweepFromAngles(double[] angles)
|
||
{
|
||
if (angles.Length < 2)
|
||
return 0;
|
||
|
||
var full = 2.0 * Math.PI;
|
||
var unwrapped = new double[angles.Length];
|
||
unwrapped[0] = angles[0];
|
||
for (int i = 1; i < angles.Length; i++)
|
||
{
|
||
var delta = angles[i] - angles[i - 1];
|
||
while (delta > Math.PI) delta -= full;
|
||
while (delta < -Math.PI) delta += full;
|
||
unwrapped[i] = unwrapped[i - 1] + delta;
|
||
}
|
||
|
||
return Math.Abs(unwrapped[unwrapped.Length - 1] - unwrapped[0]);
|
||
}
|
||
|
||
private static double GetArcSweepAngle(CircularArc3d arc, double full)
|
||
{
|
||
// 某些情况下,闭合圆会表现为“无起止点”的弧(无限弧),这里直接视为整圆
|
||
if (!arc.HasStartPoint || !arc.HasEndPoint)
|
||
return full;
|
||
|
||
var delta = arc.EndAngle - arc.StartAngle;
|
||
// 归一化到 [0, 2π]
|
||
while (delta < 0) delta += full;
|
||
while (delta > full) delta -= full;
|
||
|
||
// 特殊情况:闭合圆可能出现 start=end => delta=0
|
||
if (delta <= 1e-6)
|
||
{
|
||
try
|
||
{
|
||
if (arc.StartPoint.DistanceTo(arc.EndPoint) <= 0.1)
|
||
return full;
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
return delta;
|
||
}
|
||
|
||
/// <summary>
|
||
/// EllipticalArc3d 版本的 sweep 计算(用于“闭合圆”累计)
|
||
/// </summary>
|
||
private static double GetArcSweepAngle(EllipticalArc3d arc, double full)
|
||
{
|
||
// 某些情况下,闭合圆会表现为“无起止点”的弧(无限弧),这里直接视为整圆
|
||
if (!arc.HasStartPoint || !arc.HasEndPoint)
|
||
return full;
|
||
|
||
var delta = arc.EndAngle - arc.StartAngle;
|
||
// 归一化到 [0, 2π]
|
||
while (delta < 0) delta += full;
|
||
while (delta > full) delta -= full;
|
||
|
||
// 特殊情况:闭合圆可能出现 start=end => delta=0
|
||
if (delta <= 1e-6)
|
||
{
|
||
try
|
||
{
|
||
if (arc.StartPoint.DistanceTo(arc.EndPoint) <= 0.1)
|
||
return full;
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
return delta;
|
||
}
|
||
|
||
private static bool LooksLikeCircularEllipse(EllipticalArc3d arc, double radiusTol)
|
||
{
|
||
try
|
||
{
|
||
if (arc.IsCircular())
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
|
||
// 兜底:Major/Minor 半径接近,也可视为“圆”
|
||
return Math.Abs(arc.MajorRadius - arc.MinorRadius) <= radiusTol;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 尝试从圆柱面边界的圆弧推断孔中心与深度。
|
||
/// </summary>
|
||
private static bool TryGetHoleCenterAndDepth(
|
||
BrepFace cylinderFace,
|
||
Vector3d axis,
|
||
out Point3d center,
|
||
out double depth)
|
||
{
|
||
center = Point3d.Origin;
|
||
depth = 0;
|
||
|
||
try
|
||
{
|
||
var axisN = axis.Length > 1e-9 ? axis.GetNormal() : axis;
|
||
var circleCenters = new List<Point3d>();
|
||
var full = 2.0 * Math.PI;
|
||
|
||
foreach (var loop in cylinderFace.Loops)
|
||
{
|
||
foreach (var edge in loop.Edges)
|
||
{
|
||
foreach (var seg in EnumerateCircleArcSegments(edge.Curve, full, radiusTol: 0.2))
|
||
circleCenters.Add(seg.Center);
|
||
// 某些几何可能会返回 CompositeCurve3d,这里先不深挖;后续如遇到可再扩展
|
||
}
|
||
}
|
||
|
||
if (circleCenters.Count == 0)
|
||
return false;
|
||
|
||
// 去重:同一圆可能由多个弧段组成,中心会重复
|
||
var unique = new List<Point3d>();
|
||
const double centerTol = 0.1; // 0.1mm
|
||
foreach (var c in circleCenters)
|
||
{
|
||
bool exists = false;
|
||
foreach (var u in unique)
|
||
{
|
||
if (u.DistanceTo(c) <= centerTol)
|
||
{
|
||
exists = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!exists)
|
||
unique.Add(c);
|
||
}
|
||
|
||
if (unique.Count == 0)
|
||
return false;
|
||
|
||
if (unique.Count == 1)
|
||
{
|
||
center = unique[0];
|
||
depth = 0;
|
||
return true;
|
||
}
|
||
|
||
// 取沿轴向投影的最小/最大作为孔两端
|
||
double minT = double.MaxValue;
|
||
double maxT = double.MinValue;
|
||
Point3d minP = unique[0];
|
||
Point3d maxP = unique[0];
|
||
|
||
foreach (var p in unique)
|
||
{
|
||
var t = axisN.DotProduct(p - Point3d.Origin);
|
||
if (t < minT)
|
||
{
|
||
minT = t;
|
||
minP = p;
|
||
}
|
||
if (t > maxT)
|
||
{
|
||
maxT = t;
|
||
maxP = p;
|
||
}
|
||
}
|
||
|
||
depth = Math.Abs(maxT - minT);
|
||
center = new Point3d(
|
||
(minP.X + maxP.X) / 2.0,
|
||
(minP.Y + maxP.Y) / 2.0,
|
||
(minP.Z + maxP.Z) / 2.0
|
||
);
|
||
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static List<HoleInfo> MergeNearbyHoles(List<HoleInfo> holes)
|
||
{
|
||
if (holes.Count <= 1)
|
||
return holes;
|
||
|
||
var merged = new List<HoleInfo>();
|
||
var processed = new HashSet<int>();
|
||
|
||
const double mergeTolerance = 1.0; // 1mm
|
||
const double diameterTolerance = 0.5;
|
||
|
||
for (int i = 0; i < holes.Count; i++)
|
||
{
|
||
if (processed.Contains(i))
|
||
continue;
|
||
|
||
var current = holes[i];
|
||
processed.Add(i);
|
||
|
||
for (int j = i + 1; j < holes.Count; j++)
|
||
{
|
||
if (processed.Contains(j))
|
||
continue;
|
||
|
||
var other = holes[j];
|
||
var dx = current.Position[0] - other.Position[0];
|
||
var dy = current.Position[1] - other.Position[1];
|
||
var dz = current.Position[2] - other.Position[2];
|
||
var distance = Math.Sqrt(dx * dx + dy * dy + dz * dz);
|
||
|
||
if (distance < mergeTolerance && Math.Abs(current.Diameter - other.Diameter) < diameterTolerance)
|
||
{
|
||
processed.Add(j);
|
||
|
||
// 保留深度较大的那个
|
||
if (other.Depth > current.Depth)
|
||
{
|
||
current = other;
|
||
}
|
||
}
|
||
}
|
||
|
||
merged.Add(current);
|
||
}
|
||
|
||
return merged;
|
||
}
|
||
}
|