711 lines
24 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 System.Text.RegularExpressions;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
namespace FurnitureExporter;
/// <summary>
/// 标注孔位检测器(Fallback)
///
/// 背景:
/// - 真实 3D 打孔(布尔运算形成的圆柱面)可以通过 Brep 检测(HoleDetector/PanelAnalyzer 已实现)
/// - 但很多家具 CAD 图纸的“孔位”并不会在 Solid3d 上做减法,而是用 Circle/Block 等 2D/3D 标注表示
///
/// 这里提供一个降级方案:
/// - 扫描模型空间中的 Circle(以及 Block 内部的 Circle)
/// - 将符合孔径范围的圆,按“法向量对齐 + 贴近板件表面”的规则关联到最近板件
/// - 最终写入 PanelInfo.Holes,供导出 JSON 使用
/// </summary>
public sealed class HoleMarkerDetector
{
private readonly Database _db;
// 和 HoleDetector 保持一致的孔径范围(单位:mm)
private const double MinHoleDiameter = 2.0;
private const double MaxHoleDiameter = 100.0;
// 关联容差(单位:mm)
private const double FaceTolerance = 2.0; // 认为“在板面上”的容差
private const double BoxTolerance = 2.0; // 点在包围盒范围内的容差
// 法向量匹配阈值(|dot| 越接近 1 越平行)
private const double NormalDotThreshold = 0.90;
// 搜索范围扩展(避免标注略超出实体包围盒)
private const double SearchMargin = 50.0;
public HoleMarkerDetector(Database db)
{
_db = db;
}
/// <summary>
/// 从图纸标注(Circle/Block 内 Circle)检测孔位,并挂载到 panels[*].Holes
/// </summary>
/// <param name="panels">板件列表(来自 PanelAnalyzer)</param>
/// <param name="originOffset">导出使用的原点偏移(与板件/模型一致)</param>
/// <param name="searchMin">用于限制扫描的模型边界最小点(未偏移前的世界坐标)</param>
/// <param name="searchMax">用于限制扫描的模型边界最大点(未偏移前的世界坐标)</param>
/// <returns>本次新增的孔位列表(已合并去重,已写入 panels)</returns>
public List<HoleInfo> DetectAndAttach(
List<PanelInfo> panels,
Point3d originOffset,
Point3d searchMin,
Point3d searchMax,
IReadOnlyCollection<string>? eligiblePanelIds = null)
{
// 可关联的板件(要求有包围盒)
var candidates = panels
.Where(p => p.BoundingBox != null)
.Where(p => eligiblePanelIds == null || eligiblePanelIds.Contains(p.Id))
.Select(p => new PanelCandidate(p))
.ToList();
if (candidates.Count == 0)
return new List<HoleInfo>();
var searchExtents = ExpandExtents(new Extents3d(searchMin, searchMax), SearchMargin);
var detected = new List<HoleInfo>();
using var tr = _db.TransactionManager.StartTransaction();
var bt = (BlockTable)tr.GetObject(_db.BlockTableId, OpenMode.ForRead);
var ms = (BlockTableRecord)tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead);
// 缓存:同一个块定义只解析一次
var blockCircleCache = new Dictionary<ObjectId, List<BlockCircleMarker>>();
foreach (ObjectId id in ms)
{
var ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
if (ent == null) continue;
if (!TryGetEntityCenter(ent, out var entCenter))
{
// Circle/Block 之外的实体不处理
}
else
{
// 用中心点快速过滤搜索范围(避免全图扫描带来的性能损耗)
if (!IsPointInExtents(entCenter, searchExtents))
{
continue;
}
}
if (ent is Circle circle)
{
TryAddCircleHole(circle.Center, circle.Normal, circle.Radius, originOffset, candidates, detected);
continue;
}
if (ent is DBPoint point)
{
// 点类型孔位:常见于“孔位中心点/打孔标注”
if (LooksLikeHoleHint(point.Layer))
{
TryAddPointHole(point.Position, originOffset, candidates, point.Layer, detected);
}
continue;
}
if (ent is BlockReference br)
{
// 先用块引用自身包围盒做二次过滤
if (!TryGetGeometricExtents(br, out var brExt) || !Intersects(brExt, searchExtents))
continue;
var defId = br.BlockTableRecord;
if (!blockCircleCache.TryGetValue(defId, out var markers))
{
markers = ExtractCirclesFromBlockDefinition(defId, tr);
blockCircleCache[defId] = markers;
}
if (markers.Count == 0)
{
// 有些孔位标注是“十字/点阵/文字”的块,没有 Circle;此时把块插入点当作孔中心
if (TryGetBlockName(br, tr, out var blockName))
{
var hint = $"{br.Layer} {blockName}";
if (LooksLikeHoleHint(hint))
{
TryAddPointHole(br.Position, originOffset, candidates, hint, detected);
}
}
continue;
}
var mat = br.BlockTransform;
var scale = EstimateBlockUniformScale(br);
foreach (var m in markers)
{
var worldCenter = m.Center.TransformBy(mat);
if (!IsPointInExtents(worldCenter, searchExtents))
continue;
// 法向量只做旋转缩放变换(Vector3d.TransformBy 不受平移影响)
var worldNormal = m.Normal.TransformBy(mat);
if (worldNormal.Length > 1e-9)
worldNormal = worldNormal.GetNormal();
var worldRadius = m.Radius * scale;
TryAddCircleHole(worldCenter, worldNormal, worldRadius, originOffset, candidates, detected);
}
}
}
tr.Commit();
// 合并去重(同一孔可能被重复标注/重复扫描出来)
var merged = MergeNearbyHoles(detected);
// 写入 panels:按 panelId 分组、补齐 ID
var byPanel = merged
.Where(h => !string.IsNullOrWhiteSpace(h.PanelId))
.GroupBy(h => h.PanelId)
.ToDictionary(g => g.Key, g => g.ToList());
foreach (var panel in panels)
{
if (!byPanel.TryGetValue(panel.Id, out var holes))
continue;
// 从现有孔位数量继续编号,避免覆盖已有 ID
var startIndex = panel.Holes.Count;
for (int i = 0; i < holes.Count; i++)
{
holes[i].Id = $"{panel.Id}_hole_{(startIndex + i):D3}";
}
panel.Holes.AddRange(holes);
}
return merged;
}
private static bool TryGetEntityCenter(Entity ent, out Point3d center)
{
// 用于快速范围过滤
if (ent is Circle c)
{
center = c.Center;
return true;
}
if (ent is BlockReference br)
{
center = br.Position;
return true;
}
if (ent is DBPoint pt)
{
center = pt.Position;
return true;
}
center = Point3d.Origin;
return false;
}
private static bool TryGetGeometricExtents(Entity ent, out Extents3d ext)
{
try
{
ext = ent.GeometricExtents;
return true;
}
catch
{
ext = default;
return false;
}
}
private static bool IsPointInExtents(Point3d p, Extents3d e)
{
return p.X >= e.MinPoint.X && p.X <= e.MaxPoint.X
&& p.Y >= e.MinPoint.Y && p.Y <= e.MaxPoint.Y
&& p.Z >= e.MinPoint.Z && p.Z <= e.MaxPoint.Z;
}
private static bool Intersects(Extents3d a, Extents3d b)
{
return a.MinPoint.X <= b.MaxPoint.X && a.MaxPoint.X >= b.MinPoint.X
&& a.MinPoint.Y <= b.MaxPoint.Y && a.MaxPoint.Y >= b.MinPoint.Y
&& a.MinPoint.Z <= b.MaxPoint.Z && a.MaxPoint.Z >= b.MinPoint.Z;
}
private static Extents3d ExpandExtents(Extents3d e, double margin)
{
var min = new Point3d(e.MinPoint.X - margin, e.MinPoint.Y - margin, e.MinPoint.Z - margin);
var max = new Point3d(e.MaxPoint.X + margin, e.MaxPoint.Y + margin, e.MaxPoint.Z + margin);
return new Extents3d(min, max);
}
private static double EstimateBlockUniformScale(BlockReference br)
{
try
{
var s = br.ScaleFactors;
var sx = Math.Abs(s.X);
var sy = Math.Abs(s.Y);
var sz = Math.Abs(s.Z);
// 取平均值作为近似;家具孔位标注通常不会做非均匀缩放
return (sx + sy + sz) / 3.0;
}
catch
{
return 1.0;
}
}
private static List<BlockCircleMarker> ExtractCirclesFromBlockDefinition(ObjectId blockDefId, Transaction tr)
{
var markers = new List<BlockCircleMarker>();
try
{
var btr = (BlockTableRecord)tr.GetObject(blockDefId, OpenMode.ForRead);
foreach (ObjectId id in btr)
{
var ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
if (ent is Circle c)
{
var d = c.Radius * 2;
if (d < MinHoleDiameter || d > MaxHoleDiameter)
continue;
markers.Add(new BlockCircleMarker(c.Center, c.Normal, c.Radius));
}
}
}
catch
{
// 解析失败则忽略
}
return markers;
}
private static bool TryGetBlockName(BlockReference br, Transaction tr, out string name)
{
try
{
var btr = (BlockTableRecord)tr.GetObject(br.BlockTableRecord, OpenMode.ForRead);
name = btr.Name ?? "";
return !string.IsNullOrWhiteSpace(name);
}
catch
{
name = "";
return false;
}
}
private static bool LooksLikeHoleHint(string? text)
{
if (string.IsNullOrWhiteSpace(text))
return false;
var lower = text!.ToLowerInvariant();
// 中文/英文常见命名:孔、孔位、hole、drill
return text.Contains("孔") || lower.Contains("hole") || lower.Contains("drill");
}
private static bool TryInferDiameterFromHint(string? text, out double diameter)
{
diameter = 0;
if (string.IsNullOrWhiteSpace(text))
return false;
// 提取文本里的数字(例如:8、8.0、35 等),只接受 2~100 范围
foreach (Match m in Regex.Matches(text, @"(\d+(?:\.\d+)?)"))
{
if (!double.TryParse(m.Groups[1].Value, out var v))
continue;
if (v >= MinHoleDiameter && v <= MaxHoleDiameter)
{
diameter = v;
return true;
}
}
return false;
}
private static void TryAddPointHole(
Point3d position,
Point3d originOffset,
List<PanelCandidate> panels,
string? hint,
List<HoleInfo> output)
{
var p = new Point3d(position.X + originOffset.X, position.Y + originOffset.Y, position.Z + originOffset.Z);
var binding = FindBestPanelForPoint(p, panels);
if (binding == null)
return;
var axis = binding.PanelCandidate.Normal;
if (axis.Length > 1e-9) axis = axis.GetNormal();
var depth = binding.PanelCandidate.Panel.Size.Length >= 3 ? binding.PanelCandidate.Panel.Size[2] : 18.0;
double diameter;
if (!TryInferDiameterFromHint(hint, out diameter))
{
// 未知直径也导出(主要用于“孔位-五金件关联”,直径只影响用途推断)
diameter = 0;
}
output.Add(new HoleInfo
{
Id = "",
PanelId = binding.PanelCandidate.Panel.Id,
Position = new[] { Math.Round(p.X, 2), Math.Round(p.Y, 2), Math.Round(p.Z, 2) },
Normal = new[] { Math.Round(axis.X, 4), Math.Round(axis.Y, 4), Math.Round(axis.Z, 4) },
Diameter = Math.Round(diameter, 2),
Radius = Math.Round(diameter / 2.0, 2),
Depth = Math.Round(depth, 2),
Type = "blind",
DetectionSource = "marker",
BindingSource = binding.BindingSource,
BindingConfidence = binding.Confidence,
BindingReason = binding.BindingReason,
SurfaceFaces = PanelFaceSemanticResolver.ResolveSurfaceFaces(
binding.PanelCandidate.Panel.BoundingBox,
new[] { p.X, p.Y, p.Z },
new[] { axis.X, axis.Y, axis.Z },
depth),
});
}
private static void TryAddCircleHole(
Point3d center,
Vector3d normal,
double radius,
Point3d originOffset,
List<PanelCandidate> panels,
List<HoleInfo> output)
{
var diameter = radius * 2.0;
if (diameter < MinHoleDiameter || diameter > MaxHoleDiameter)
return;
var axis = normal;
if (axis.Length < 1e-9)
return;
axis = axis.GetNormal();
// 坐标对齐:面板/模型已经应用 originOffset,这里也要一致
var p = new Point3d(center.X + originOffset.X, center.Y + originOffset.Y, center.Z + originOffset.Z);
var binding = FindBestPanelForHole(p, axis, panels);
if (binding == null)
return;
var depth = binding.PanelCandidate.Panel.Size.Length >= 3 ? binding.PanelCandidate.Panel.Size[2] : 18.0;
output.Add(new HoleInfo
{
// ID 在最后统一补齐(避免与现有孔位冲突)
Id = "",
PanelId = binding.PanelCandidate.Panel.Id,
Position = new[] { Math.Round(p.X, 2), Math.Round(p.Y, 2), Math.Round(p.Z, 2) },
Normal = new[] { Math.Round(axis.X, 4), Math.Round(axis.Y, 4), Math.Round(axis.Z, 4) },
Diameter = Math.Round(diameter, 2),
Radius = Math.Round(radius, 2),
Depth = Math.Round(depth, 2),
Type = "blind",
DetectionSource = "marker",
BindingSource = binding.BindingSource,
BindingConfidence = binding.Confidence,
BindingReason = binding.BindingReason,
SurfaceFaces = PanelFaceSemanticResolver.ResolveSurfaceFaces(
binding.PanelCandidate.Panel.BoundingBox,
new[] { p.X, p.Y, p.Z },
new[] { axis.X, axis.Y, axis.Z },
depth),
});
}
private static PanelBindingResult? FindBestPanelForHole(Point3d p, Vector3d axis, List<PanelCandidate> panels)
{
PanelCandidate? best = null;
string bestBindingSource = "marker_box";
string bestBindingReason = "按标注孔包围盒退化匹配板件";
double bestScore = double.MaxValue;
// 两阶段:优先贴面(更像真实孔位标注),否则退回到“在包围盒内”
for (int pass = 0; pass < 2; pass++)
{
foreach (var panel in panels)
{
var dot = Math.Abs(panel.Normal.DotProduct(axis));
if (dot < NormalDotThreshold)
continue;
if (pass == 0)
{
if (!IsPointOnPanelFace(p, panel.Extents, panel.Normal, FaceTolerance, BoxTolerance))
continue;
}
else
{
if (!IsPointInsideBox(p, panel.Extents, BoxTolerance))
continue;
}
// 以到包围盒的距离作为评分(越小越好)
var dist = DistancePointToBox(p, panel.Extents);
// 同等距离下,dot 越大越好
var score = dist + (1.0 - dot) * 10.0;
if (score < bestScore)
{
bestScore = score;
best = panel;
bestBindingSource = pass == 0 ? "marker_face" : "marker_box";
bestBindingReason = pass == 0
? "按标注孔贴面匹配板件"
: "按标注孔包围盒退化匹配板件";
}
}
if (best != null)
return new PanelBindingResult(
best,
bestBindingSource,
ComputeBindingConfidence(bestScore, pass == 0)
,
bestBindingReason
);
}
return null;
}
private static PanelBindingResult? FindBestPanelForPoint(Point3d p, List<PanelCandidate> panels)
{
PanelCandidate? best = null;
double bestDist = double.MaxValue;
// 优先贴面(更像真实孔位标注)
foreach (var panel in panels)
{
if (!IsPointOnPanelFace(p, panel.Extents, panel.Normal, FaceTolerance, BoxTolerance))
continue;
var dist = DistancePointToBox(p, panel.Extents);
if (dist < bestDist)
{
bestDist = dist;
best = panel;
}
}
if (best != null)
return new PanelBindingResult(
best,
"marker_face",
ComputeBindingConfidence(bestDist, true),
"按标注点贴面匹配板件"
);
// 退化:在包围盒内
foreach (var panel in panels)
{
if (!IsPointInsideBox(p, panel.Extents, BoxTolerance))
continue;
var dist = DistancePointToBox(p, panel.Extents);
if (dist < bestDist)
{
bestDist = dist;
best = panel;
}
}
if (best == null)
return null;
return new PanelBindingResult(
best,
"marker_box",
ComputeBindingConfidence(bestDist, false),
"按标注点包围盒退化匹配板件"
);
}
private static double ComputeBindingConfidence(double score, bool faceMatched)
{
var normalized = Math.Max(0.0, 1.0 - score / 10.0);
var baseConfidence = faceMatched ? 0.88 : 0.58;
var range = faceMatched ? 0.12 : 0.22;
return Math.Round(Math.Min(1.0, baseConfidence + normalized * range), 4);
}
private static bool IsPointInsideBox(Point3d p, Extents3d box, double tol)
{
return p.X >= box.MinPoint.X - tol && p.X <= box.MaxPoint.X + tol
&& p.Y >= box.MinPoint.Y - tol && p.Y <= box.MaxPoint.Y + tol
&& p.Z >= box.MinPoint.Z - tol && p.Z <= box.MaxPoint.Z + tol;
}
private static bool IsPointOnPanelFace(Point3d p, Extents3d box, Vector3d panelNormal, double faceTol, double rangeTol)
{
// 由于 PanelAnalyzer 的 Normal 是轴对齐向量(1/0),这里直接按主轴判断
var ax = Math.Abs(panelNormal.X);
var ay = Math.Abs(panelNormal.Y);
var az = Math.Abs(panelNormal.Z);
if (ax >= ay && ax >= az)
{
if (p.Y < box.MinPoint.Y - rangeTol || p.Y > box.MaxPoint.Y + rangeTol) return false;
if (p.Z < box.MinPoint.Z - rangeTol || p.Z > box.MaxPoint.Z + rangeTol) return false;
return Math.Abs(p.X - box.MinPoint.X) <= faceTol || Math.Abs(p.X - box.MaxPoint.X) <= faceTol;
}
if (ay >= ax && ay >= az)
{
if (p.X < box.MinPoint.X - rangeTol || p.X > box.MaxPoint.X + rangeTol) return false;
if (p.Z < box.MinPoint.Z - rangeTol || p.Z > box.MaxPoint.Z + rangeTol) return false;
return Math.Abs(p.Y - box.MinPoint.Y) <= faceTol || Math.Abs(p.Y - box.MaxPoint.Y) <= faceTol;
}
// Z
if (p.X < box.MinPoint.X - rangeTol || p.X > box.MaxPoint.X + rangeTol) return false;
if (p.Y < box.MinPoint.Y - rangeTol || p.Y > box.MaxPoint.Y + rangeTol) return false;
return Math.Abs(p.Z - box.MinPoint.Z) <= faceTol || Math.Abs(p.Z - box.MaxPoint.Z) <= faceTol;
}
private static double DistancePointToBox(Point3d p, Extents3d box)
{
double dx = 0, dy = 0, dz = 0;
if (p.X < box.MinPoint.X) dx = box.MinPoint.X - p.X;
else if (p.X > box.MaxPoint.X) dx = p.X - box.MaxPoint.X;
if (p.Y < box.MinPoint.Y) dy = box.MinPoint.Y - p.Y;
else if (p.Y > box.MaxPoint.Y) dy = p.Y - box.MaxPoint.Y;
if (p.Z < box.MinPoint.Z) dz = box.MinPoint.Z - p.Z;
else if (p.Z > box.MaxPoint.Z) dz = p.Z - box.MaxPoint.Z;
return Math.Sqrt(dx * dx + dy * dy + dz * dz);
}
private static List<HoleInfo> MergeNearbyHoles(List<HoleInfo> holes)
{
if (holes.Count <= 1)
return holes;
const double mergeTolerance = 1.0; // 1mm
var merged = new List<HoleInfo>();
var used = new bool[holes.Count];
for (int i = 0; i < holes.Count; i++)
{
if (used[i]) continue;
var current = holes[i];
used[i] = true;
for (int j = i + 1; j < holes.Count; j++)
{
if (used[j]) continue;
var other = holes[j];
if (!string.Equals(current.PanelId, other.PanelId, StringComparison.OrdinalIgnoreCase))
continue;
var dist = 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)
);
var diameterCompatible =
current.Diameter <= 0 || other.Diameter <= 0 ||
Math.Abs(current.Diameter - other.Diameter) <= 0.5;
if (dist <= mergeTolerance && diameterCompatible)
{
used[j] = true;
// 保留深度更大的(信息更多)
if (other.Depth > current.Depth)
current = other;
}
}
merged.Add(current);
}
return merged;
}
private sealed class PanelCandidate
{
public PanelInfo Panel { get; }
public Extents3d Extents { get; }
public Vector3d Normal { get; }
public PanelCandidate(PanelInfo panel)
{
Panel = panel;
Extents = ToExtents(panel.BoundingBox!);
Normal = ToVector(panel.Normal);
if (Normal.Length > 1e-9) Normal = Normal.GetNormal();
}
}
private sealed class PanelBindingResult
{
public PanelCandidate PanelCandidate { get; }
public string BindingSource { get; }
public double Confidence { get; }
public string BindingReason { get; }
public PanelBindingResult(
PanelCandidate panelCandidate,
string bindingSource,
double confidence,
string bindingReason)
{
PanelCandidate = panelCandidate;
BindingSource = bindingSource;
Confidence = confidence;
BindingReason = bindingReason;
}
}
private sealed class BlockCircleMarker
{
public Point3d Center { get; }
public Vector3d Normal { get; }
public double Radius { get; }
public BlockCircleMarker(Point3d center, Vector3d normal, double radius)
{
Center = center;
Normal = normal;
Radius = radius;
}
}
private static Extents3d ToExtents(BoundingBox3D box)
{
var min = new Point3d(box.Min[0], box.Min[1], box.Min[2]);
var max = new Point3d(box.Max[0], box.Max[1], box.Max[2]);
return new Extents3d(min, max);
}
private static Vector3d ToVector(double[] arr)
{
if (arr.Length < 3) return new Vector3d(0, 0, 1);
return new Vector3d(arr[0], arr[1], arr[2]);
}
}