1626 lines
56 KiB
C#
Raw Permalink 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 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;
}
}