包含: - furniture-3d-system:Next.js 16 全栈应用(资产中心、产品管理、3D装配动画) - autocad-plugin:AutoCAD 家具导出插件源码 - 资产中心改造:编辑弹窗化、删除功能、Toast反馈、自定义Select组件 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
354 lines
12 KiB
C#
354 lines
12 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>
|
||
/// 孔位检测器 - 从板材实体中提取圆柱形孔位
|
||
/// 通过分析 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
|
||
{
|
||
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)
|
||
};
|
||
}
|
||
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";
|
||
}
|
||
}
|