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; } }