214 lines
7.5 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;
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;
}
}