711 lines
24 KiB
C#
711 lines
24 KiB
C#
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]);
|
||
}
|
||
}
|