using System;
using System.Collections.Generic;
using System.Linq;
namespace FurnitureExporter;
///
/// 孔位分组(逻辑孔位)
///
/// 背景:
/// - CAD 几何层面:偏心件(偏心螺丝/三合一连接件)常常表现为“两个互相垂直的圆柱孔”(例如 8mm 侧孔 + 12/15mm 主孔);
/// - 业务层面:设计师/工艺人员通常把这一套当成 1 个“孔位”,用于计数、展示与五金匹配;
/// - 因此这里把相关几何孔合并成 HoleGroup,同时在 HoleInfo 上写入 HoleGroupId。
///
public static class HoleGrouping
{
///
/// 根据 HoleGroupId 计算“逻辑孔位数量”
///
public static int ComputeLogicalHoleCount(IEnumerable holes)
{
return holes
.Select(h => string.IsNullOrWhiteSpace(h.HoleGroupId) ? h.Id : h.HoleGroupId!)
.Distinct(StringComparer.OrdinalIgnoreCase)
.Count();
}
///
/// 识别并生成“偏心螺丝孔位”分组
///
/// 规则(基于当前样例与常见家具连接件):
/// - 小孔:直径约 8mm(7.5~8.5)
/// - 主孔:直径约 12~16mm(11~16.5),与小孔法向大致垂直
/// - 空间距离:中心距离在阈值内(默认 45mm)
///
public static List BuildEccentricScrewGroups(IReadOnlyList holes)
{
var groups = new List();
// 每个板件独立分组,避免跨板件误配
var byPanel = holes
.Where(h => !string.IsNullOrWhiteSpace(h.PanelId))
.GroupBy(h => h.PanelId, StringComparer.OrdinalIgnoreCase);
foreach (var panelGroup in byPanel)
{
var panelId = panelGroup.Key;
var panelHoles = panelGroup.ToList();
var small = panelHoles
.Where(h => h.Diameter >= 7.5 && h.Diameter <= 8.5)
.ToList();
var main = panelHoles
.Where(h => h.Diameter >= 11.0 && h.Diameter <= 16.5)
.ToList();
if (small.Count == 0 || main.Count == 0)
continue;
var usedSmall = new HashSet(StringComparer.OrdinalIgnoreCase);
var usedMain = new HashSet(StringComparer.OrdinalIgnoreCase);
int groupIndex = 0;
foreach (var mainHole in main)
{
if (usedMain.Contains(mainHole.Id))
continue;
var best = FindBestSmallHole(mainHole, small, usedSmall, maxDistanceMm: 45.0);
if (best == null)
continue;
usedMain.Add(mainHole.Id);
usedSmall.Add(best.Id);
var groupId = $"{panelId}_group_{groupIndex:D3}";
groupIndex++;
var dist = Distance(mainHole.Position, best.Position);
var dot = Math.Abs(Dot(mainHole.Normal, best.Normal));
var confidence = ComputeConfidence(dist, dot, maxDistanceMm: 45.0);
// 记录分组
var holeIds = new[] { mainHole.Id, best.Id };
var group = new HoleGroupInfo
{
Id = groupId,
PanelId = panelId,
GroupType = "eccentric_screw",
HoleIds = holeIds,
Position = mainHole.Position,
Normal = mainHole.Normal,
Confidence = confidence,
Reason = $"主孔直径 {mainHole.Diameter:0.#}mm + 侧孔直径 {best.Diameter:0.#}mm,距离 {dist:0.#}mm,法向点积 {dot:0.00}",
MountFace = mainHole.SurfaceFaces.FirstOrDefault(face => !string.IsNullOrWhiteSpace(face)) ?? "unknown",
SurfaceFaces = mainHole.SurfaceFaces
.Concat(best.SurfaceFaces ?? Array.Empty())
.Where(face => !string.IsNullOrWhiteSpace(face))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray(),
};
groups.Add(group);
// 标记成员孔:同一个逻辑孔位
MarkGroupMember(mainHole, groupId, "eccentric_screw", role: "main");
MarkGroupMember(best, groupId, "eccentric_screw", role: "side");
// 偏心孔位场景下,单纯按直径推断(dowel_8/unknown)容易误导,这里统一覆盖成 eccentric_screw
mainHole.InferredUsage = "eccentric_screw";
best.InferredUsage = "eccentric_screw";
}
}
return groups;
}
///
/// 把分组写回到 PanelInfo,方便按板件查看
///
public static void AttachGroupsToPanels(IReadOnlyList panels, IReadOnlyList groups)
{
var map = groups
.GroupBy(g => g.PanelId, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);
foreach (var panel in panels)
{
if (map.TryGetValue(panel.Id, out var list))
{
panel.HoleGroups = list;
}
else
{
panel.HoleGroups = new List();
}
}
}
private static HoleInfo? FindBestSmallHole(
HoleInfo mainHole,
IReadOnlyList smallCandidates,
HashSet usedSmall,
double maxDistanceMm)
{
HoleInfo? best = null;
double bestScore = double.NegativeInfinity;
foreach (var s in smallCandidates)
{
if (usedSmall.Contains(s.Id))
continue;
// 法向需要接近垂直:|dot| 越小越好
var dot = Math.Abs(Dot(mainHole.Normal, s.Normal));
if (dot > 0.35) // 约 70 度以内就不算“明显垂直”
continue;
var dist = Distance(mainHole.Position, s.Position);
if (dist > maxDistanceMm)
continue;
// 评分:越近越好,越垂直越好
var score = (1.0 - (dist / maxDistanceMm)) + (1.0 - dot);
if (score > bestScore)
{
bestScore = score;
best = s;
}
}
return best;
}
private static void MarkGroupMember(HoleInfo hole, string groupId, string groupType, string role)
{
hole.HoleGroupId = groupId;
hole.HoleGroupType = groupType;
hole.HoleGroupRole = role;
}
private static double ComputeConfidence(double dist, double absDot, double maxDistanceMm)
{
// dist: 0 -> 1, maxDistance -> 0
var distScore = Clamp(1.0 - (dist / maxDistanceMm), 0.0, 1.0);
// absDot: 0(垂直) -> 1, 1(平行) -> 0
var orthoScore = Clamp(1.0 - absDot, 0.0, 1.0);
return Math.Round(0.6 * orthoScore + 0.4 * distScore, 4);
}
private static double Distance(double[] a, double[] b)
{
var dx = a[0] - b[0];
var dy = a[1] - b[1];
var dz = a[2] - b[2];
return Math.Sqrt(dx * dx + dy * dy + dz * dz);
}
private static double Dot(double[] a, double[] b)
{
if (a.Length < 3 || b.Length < 3)
return 0;
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
private static double Clamp(double value, double min, double max)
{
if (value < min) return min;
if (value > max) return max;
return value;
}
}