214 lines
7.5 KiB
C#
214 lines
7.5 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
|
||
namespace FurnitureExporter;
|
||
|
||
/// <summary>
|
||
/// 孔位分组(逻辑孔位)
|
||
///
|
||
/// 背景:
|
||
/// - CAD 几何层面:偏心件(偏心螺丝/三合一连接件)常常表现为“两个互相垂直的圆柱孔”(例如 8mm 侧孔 + 12/15mm 主孔);
|
||
/// - 业务层面:设计师/工艺人员通常把这一套当成 1 个“孔位”,用于计数、展示与五金匹配;
|
||
/// - 因此这里把相关几何孔合并成 HoleGroup,同时在 HoleInfo 上写入 HoleGroupId。
|
||
/// </summary>
|
||
public static class HoleGrouping
|
||
{
|
||
/// <summary>
|
||
/// 根据 HoleGroupId 计算“逻辑孔位数量”
|
||
/// </summary>
|
||
public static int ComputeLogicalHoleCount(IEnumerable<HoleInfo> holes)
|
||
{
|
||
return holes
|
||
.Select(h => string.IsNullOrWhiteSpace(h.HoleGroupId) ? h.Id : h.HoleGroupId!)
|
||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||
.Count();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 识别并生成“偏心螺丝孔位”分组
|
||
///
|
||
/// 规则(基于当前样例与常见家具连接件):
|
||
/// - 小孔:直径约 8mm(7.5~8.5)
|
||
/// - 主孔:直径约 12~16mm(11~16.5),与小孔法向大致垂直
|
||
/// - 空间距离:中心距离在阈值内(默认 45mm)
|
||
/// </summary>
|
||
public static List<HoleGroupInfo> BuildEccentricScrewGroups(IReadOnlyList<HoleInfo> holes)
|
||
{
|
||
var groups = new List<HoleGroupInfo>();
|
||
|
||
// 每个板件独立分组,避免跨板件误配
|
||
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<string>(StringComparer.OrdinalIgnoreCase);
|
||
var usedMain = new HashSet<string>(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<string>())
|
||
.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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 把分组写回到 PanelInfo,方便按板件查看
|
||
/// </summary>
|
||
public static void AttachGroupsToPanels(IReadOnlyList<PanelInfo> panels, IReadOnlyList<HoleGroupInfo> 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<HoleGroupInfo>();
|
||
}
|
||
}
|
||
}
|
||
|
||
private static HoleInfo? FindBestSmallHole(
|
||
HoleInfo mainHole,
|
||
IReadOnlyList<HoleInfo> smallCandidates,
|
||
HashSet<string> 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;
|
||
}
|
||
}
|
||
|