384 lines
13 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 Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.BoundaryRepresentation;
using BrepFace = Autodesk.AutoCAD.BoundaryRepresentation.Face;
namespace FurnitureExporter;
/// <summary>
/// 孔位检测器 - 从板材实体中提取圆柱形孔位
/// 通过分析 Solid3d 的 Brep(边界表示)来识别布尔运算产生的孔
/// </summary>
public class HoleDetector
{
private readonly Database _db;
private Point3d _originOffset = Point3d.Origin;
// 孔位检测参数
private const double MIN_HOLE_DIAMETER = 2; // 最小孔径 2mm
private const double MAX_HOLE_DIAMETER = 100; // 最大孔径 100mm
private const double MIN_HOLE_DEPTH = 3; // 最小孔深 3mm
public HoleDetector(Database db)
{
_db = db;
}
/// <summary>
/// 从板材实体中检测所有孔位
/// </summary>
/// <param name="solidId">板材实体 ID</param>
/// <param name="panelId">板材 ID</param>
/// <param name="originOffset">原点偏移</param>
/// <returns>孔位信息列表</returns>
public List<HoleInfo> DetectHoles(ObjectId solidId, string panelId, Point3d? originOffset = null)
{
_originOffset = originOffset ?? Point3d.Origin;
var holes = new List<HoleInfo>();
using var tr = _db.TransactionManager.StartTransaction();
var solid = tr.GetObject(solidId, OpenMode.ForRead) as Solid3d;
if (solid == null)
{
tr.Commit();
return holes;
}
holes = DetectHolesFromSolid(solid, panelId);
tr.Commit();
return holes;
}
/// <summary>
/// 从多个板材实体中检测所有孔位
/// </summary>
public List<HoleInfo> DetectAllHoles(ObjectId[] solidIds, List<PanelInfo> panels, Point3d? originOffset = null)
{
_originOffset = originOffset ?? Point3d.Origin;
var allHoles = new List<HoleInfo>();
using var tr = _db.TransactionManager.StartTransaction();
for (int i = 0; i < solidIds.Length && i < panels.Count; i++)
{
var solid = tr.GetObject(solidIds[i], OpenMode.ForRead) as Solid3d;
if (solid == null) continue;
var holes = DetectHolesFromSolid(solid, panels[i].Id);
allHoles.AddRange(holes);
}
tr.Commit();
return allHoles;
}
/// <summary>
/// 从单个 Solid3d 实体检测孔位
/// </summary>
private List<HoleInfo> DetectHolesFromSolid(Solid3d solid, string panelId)
{
var holes = new List<HoleInfo>();
try
{
// 使用 Brep API 分析实体表面
using var brep = new Brep(solid);
// 收集所有圆柱形面
var cylindricalFaces = new List<(BrepFace Face, Cylinder Cylinder)>();
foreach (var face in brep.Faces)
{
try
{
// face.Surface 常常是 ExternalBoundedSurface 等包装类型,需剥离到基础几何表面
var surface = BrepSurfaceUtils.Unwrap(face.Surface);
if (surface is Cylinder cylinder)
{
// 验证是否为内凹面(孔)而非外凸面
if (IsConcaveCylinder(face, cylinder))
{
cylindricalFaces.Add((face, cylinder));
}
}
}
catch
{
// 某些面可能无法获取表面信息,跳过
continue;
}
}
// 将相同圆柱配对(孔通常有两个端面)
var processedFaces = new HashSet<int>();
int holeIndex = 0;
foreach (var (face, cylinder) in cylindricalFaces)
{
if (processedFaces.Contains(face.GetHashCode()))
continue;
var hole = ExtractHoleInfo(face, cylinder, solid, panelId, holeIndex);
if (hole != null && IsValidHole(hole))
{
holes.Add(hole);
holeIndex++;
}
processedFaces.Add(face.GetHashCode());
}
// 去重(合并相近的孔)
holes = MergeNearbyHoles(holes);
}
catch (System.Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[HoleDetector] Error: {ex.Message}");
}
return holes;
}
/// <summary>
/// 判断圆柱面是否为内凹(孔)
/// </summary>
private bool IsConcaveCylinder(BrepFace face, Cylinder cylinder)
{
// 简化判断:根据圆柱半径和板材尺寸判断是否可能是孔
// 圆柱半径在合理范围内即认为是孔
var radius = cylinder.Radius;
return radius >= MIN_HOLE_DIAMETER / 2 && radius <= MAX_HOLE_DIAMETER / 2;
}
/// <summary>
/// 从圆柱面提取孔位信息
/// </summary>
private HoleInfo? ExtractHoleInfo(BrepFace face, Cylinder cylinder, Solid3d solid, string panelId, int index)
{
try
{
BoundingBox3D? panelBoundingBox = null;
try
{
var ext = solid.GeometricExtents;
panelBoundingBox = new BoundingBox3D
{
Min = new[]
{
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[]
{
Math.Round(ext.MaxPoint.X + _originOffset.X, 2),
Math.Round(ext.MaxPoint.Y + _originOffset.Y, 2),
Math.Round(ext.MaxPoint.Z + _originOffset.Z, 2),
},
};
}
catch
{
panelBoundingBox = null;
}
var radius = cylinder.Radius;
var diameter = radius * 2;
// 获取圆柱的轴向(孔的方向)
var axis = cylinder.AxisOfSymmetry.GetNormal();
// 获取孔的位置
var origin = cylinder.Origin;
// 计算孔深度
var depth = CalculateHoleDepth(face, solid, cylinder);
// 应用原点偏移
var offsetPosition = new Point3d(
origin.X + _originOffset.X,
origin.Y + _originOffset.Y,
origin.Z + _originOffset.Z
);
// 判断孔类型(通孔/盲孔)
string holeType = DetermineHoleType(face, solid, cylinder, depth);
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,
Radius = Math.Round(radius, 2),
SurfaceFaces = PanelFaceSemanticResolver.ResolveSurfaceFaces(
panelBoundingBox,
new[] { offsetPosition.X, offsetPosition.Y, offsetPosition.Z },
new[] { axis.X, axis.Y, axis.Z },
depth)
};
}
catch (System.Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[HoleDetector] ExtractHoleInfo error: {ex.Message}");
return null;
}
}
/// <summary>
/// 计算孔深度
/// </summary>
private double CalculateHoleDepth(BrepFace face, Solid3d solid, Cylinder cylinder)
{
try
{
// 使用板材厚度作为孔深度的参考
var solidExt = solid.GeometricExtents;
var solidSize = solidExt.MaxPoint - solidExt.MinPoint;
var thickness = Math.Min(solidSize.X, Math.Min(solidSize.Y, solidSize.Z));
// 默认返回板材厚度(假设为通孔)
return thickness;
}
catch
{
return 0;
}
}
/// <summary>
/// 判断孔类型(通孔/盲孔)
/// </summary>
private string DetermineHoleType(BrepFace face, Solid3d solid, Cylinder cylinder, double depth)
{
try
{
var solidExt = solid.GeometricExtents;
var solidSize = solidExt.MaxPoint - solidExt.MinPoint;
// 获取板材厚度(最小尺寸)
var thickness = Math.Min(solidSize.X, Math.Min(solidSize.Y, solidSize.Z));
// 如果孔深度接近板材厚度(误差 2mm),认为是通孔
if (Math.Abs(depth - thickness) < 2)
{
return "through";
}
return "blind";
}
catch
{
return "blind";
}
}
/// <summary>
/// 验证孔位是否有效
/// </summary>
private bool IsValidHole(HoleInfo hole)
{
// 检查直径范围
if (hole.Diameter < MIN_HOLE_DIAMETER || hole.Diameter > MAX_HOLE_DIAMETER)
return false;
// 检查深度
if (hole.Depth < MIN_HOLE_DEPTH)
return false;
return true;
}
/// <summary>
/// 合并相近的孔位(去重)
/// </summary>
private List<HoleInfo> MergeNearbyHoles(List<HoleInfo> holes)
{
if (holes.Count <= 1)
return holes;
var merged = new List<HoleInfo>();
var processed = new HashSet<int>();
const double MERGE_TOLERANCE = 1.0; // 1mm 容差
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 distance = Math.Sqrt(
Math.Pow(current.Position[0] - other.Position[0], 2) +
Math.Pow(current.Position[1] - other.Position[1], 2) +
Math.Pow(current.Position[2] - other.Position[2], 2)
);
// 如果位置相近且直径相同,认为是同一个孔
if (distance < MERGE_TOLERANCE && Math.Abs(current.Diameter - other.Diameter) < 0.5)
{
processed.Add(j);
// 保留深度较大的那个
if (other.Depth > current.Depth)
{
current = other;
}
}
}
merged.Add(current);
}
return merged;
}
/// <summary>
/// 根据直径推断孔类型(五金件类型)
/// </summary>
public static string InferHoleUsage(double diameter)
{
// 从大到小、从具体到模糊排列,避免匹配重叠
if (diameter >= 34 && diameter <= 36) return "hinge"; // 铰链杯孔
if (diameter >= 20 && diameter <= 100) return "cable"; // 过线孔
if (diameter >= 14 && diameter <= 16) return "connector"; // 偏心轮孔
if (diameter >= 9.5 && diameter <= 10.5) return "dowel_10"; // 木榫孔 10mm
if (diameter >= 7.5 && diameter <= 8.5) return "dowel_8"; // 木榫孔 8mm
if (diameter >= 7 && diameter <= 9) return "connector_small"; // 三合一孔
if (diameter >= 5.5 && diameter <= 6.5) return "dowel_6"; // 木榫孔 6mm
// v2.3:孔径 4.8mm 在实际导出中非常常见(更像 M6 的导孔/预钻孔),
// 仅靠“孔径”无法稳定区分“层板托孔”和“螺丝导孔”,因此这里返回更中性的用途,
// 具体候选螺丝直径由 FastenerGuesser 在 hole.SuggestedFasteners 中给出。
if (diameter >= 4.5 && diameter <= 5.3) return "screw_or_shelf_pin";
if (diameter >= 3.0 && diameter < 4.5) return "screw"; // 螺丝预钻孔(常见给 M4/M5)
if (diameter >= 2.0 && diameter < 3.0) return "micro_screw"; // 小螺丝/定位孔(保守兜底)
return "unknown";
}
}