using System;
using System.Collections.Generic;
using System.Linq;
namespace FurnitureExporter;
///
/// 安装图样识别器。
///
/// 设计口径:
/// - 吸附面(mount face)先缩小候选范围;
/// - HoleGroup 负责把几何孔组织成业务孔位;
/// - MountPattern 负责定义“至少命中几个孔才允许吸附成立”。
///
public static class MountPatternAnalyzer
{
private const double DefaultClusterDistanceMm = 220.0;
public static List BuildMountPatterns(
IReadOnlyList panels,
IReadOnlyList holes,
IReadOnlyList holeGroups)
{
var patterns = new List();
if (panels == null || panels.Count == 0)
{
return patterns;
}
var holesByPanel = holes
.Where(h => !string.IsNullOrWhiteSpace(h.PanelId))
.GroupBy(h => h.PanelId, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.ToList(), StringComparer.OrdinalIgnoreCase);
var groupsByPanel = holeGroups
.Where(group => !string.IsNullOrWhiteSpace(group.PanelId))
.GroupBy(group => group.PanelId, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.ToList(), StringComparer.OrdinalIgnoreCase);
foreach (var panel in panels)
{
var panelHoles = holesByPanel.TryGetValue(panel.Id, out var panelHoleList)
? panelHoleList
: new List();
var panelGroups = groupsByPanel.TryGetValue(panel.Id, out var panelGroupList)
? panelGroupList
: new List();
var units = BuildUnits(panel.Id, panelHoles, panelGroups);
if (units.Count == 0)
{
panel.MountPatterns = new List();
panel.SnapPatterns = new List();
continue;
}
var panelPatterns = new List();
var unitsByFace = units
.SelectMany(unit =>
unit.MountFaces.Select(face => new { Face = face, Unit = unit }))
.GroupBy(item => item.Face, StringComparer.OrdinalIgnoreCase);
foreach (var faceGroup in unitsByFace)
{
var clusters = BuildClusters(faceGroup.Select(item => item.Unit).Distinct().ToList());
var patternIndex = 0;
foreach (var cluster in clusters)
{
var pattern = BuildPattern(panel.Id, faceGroup.Key, patternIndex++, cluster);
panelPatterns.Add(pattern);
}
}
panel.MountPatterns = panelPatterns;
panel.SnapPatterns = panelPatterns;
patterns.AddRange(panelPatterns);
}
return patterns;
}
public static void AttachPatternsToPanels(IReadOnlyList panels, IReadOnlyList patterns)
{
var map = patterns
.GroupBy(pattern => pattern.PanelId, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.ToList(), StringComparer.OrdinalIgnoreCase);
foreach (var panel in panels)
{
var panelPatterns = map.TryGetValue(panel.Id, out var items)
? items
: new List();
panel.MountPatterns = panelPatterns;
panel.SnapPatterns = panelPatterns;
}
}
private static List BuildUnits(
string panelId,
IReadOnlyList holes,
IReadOnlyList holeGroups)
{
var units = new List();
var groupedHoleIds = new HashSet(StringComparer.OrdinalIgnoreCase);
foreach (var group in holeGroups)
{
foreach (var holeId in group.HoleIds ?? Array.Empty())
{
groupedHoleIds.Add(holeId);
}
var mountFaces = NormalizeMountFaces(group.SurfaceFaces, group.MountFace);
group.MountFace = mountFaces.FirstOrDefault() ?? "unknown";
units.Add(new MountPatternUnit
{
Id = group.Id,
PanelId = panelId,
HoleIds = group.HoleIds ?? Array.Empty(),
Position = group.Position,
Normal = group.Normal,
GroupType = group.GroupType,
MountFaces = mountFaces,
Confidence = Clamp01(group.Confidence <= 0 ? 0.72 : group.Confidence),
Warnings = group.Warnings?.ToList() ?? new List(),
Source = "hole-group",
});
}
foreach (var hole in holes)
{
if (groupedHoleIds.Contains(hole.Id))
{
continue;
}
units.Add(new MountPatternUnit
{
Id = hole.Id,
PanelId = panelId,
HoleIds = new[] { hole.Id },
Position = hole.Position,
Normal = hole.Normal,
GroupType = hole.HoleGroupType,
MountFaces = NormalizeMountFaces(hole.SurfaceFaces),
Confidence = Clamp01(hole.Confidence <= 0 ? 0.76 : hole.Confidence),
Warnings = hole.Warnings?.ToList() ?? new List(),
Source = "single-hole",
});
}
return units;
}
private static List> BuildClusters(IReadOnlyList units)
{
var clusters = new List>();
var visited = new HashSet(StringComparer.OrdinalIgnoreCase);
foreach (var unit in units)
{
if (!visited.Add(unit.Id))
{
continue;
}
var cluster = new List();
var queue = new Queue();
queue.Enqueue(unit);
while (queue.Count > 0)
{
var current = queue.Dequeue();
cluster.Add(current);
foreach (var candidate in units)
{
if (visited.Contains(candidate.Id))
{
continue;
}
if (Distance(current.Position, candidate.Position) > DefaultClusterDistanceMm)
{
continue;
}
visited.Add(candidate.Id);
queue.Enqueue(candidate);
}
}
clusters.Add(cluster.OrderBy(item => item.Position[0]).ThenBy(item => item.Position[1]).ThenBy(item => item.Position[2]).ToList());
}
return clusters;
}
private static MountPatternInfo BuildPattern(
string panelId,
string mountFace,
int patternIndex,
IReadOnlyList cluster)
{
var holeIds = cluster
.SelectMany(unit => unit.HoleIds)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var warnings = cluster
.SelectMany(unit => unit.Warnings)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
var pointCount = cluster.Count;
var patternType = ResolvePatternType(cluster);
var isSymmetricRectangle = string.Equals(patternType, "rectangle_4", StringComparison.OrdinalIgnoreCase);
var minRequiredHoles = ResolveMinRequiredHoleCount(patternType, pointCount, isSymmetricRectangle);
var preferredHoleCount = pointCount;
var requiredHoleIds = Array.Empty();
var optionalHoleIds = Array.Empty();
if (pointCount <= 3 || !isSymmetricRectangle)
{
requiredHoleIds = holeIds;
}
else
{
optionalHoleIds = holeIds;
warnings.Add("四孔对称图样默认按 2 孔完成初始定位,剩余孔位作为增强固定。");
}
if (string.Equals(patternType, "generic", StringComparison.OrdinalIgnoreCase))
{
warnings.Add("当前图样由几何聚类自动生成,建议后续在资产录入阶段人工复核。");
}
return new MountPatternInfo
{
Id = $"{panelId}_mount_{mountFace}_{patternIndex:D3}",
PanelId = panelId,
MountFace = mountFace,
PatternType = patternType,
HoleIds = holeIds,
RequiredHoleIds = requiredHoleIds,
OptionalHoleIds = optionalHoleIds,
MinRequiredHoles = minRequiredHoles,
PreferredHoleCount = preferredHoleCount,
Symmetry = isSymmetricRectangle,
Position = AveragePosition(cluster),
Normal = ResolveMountFaceNormal(mountFace),
Confidence = ComputePatternConfidence(cluster, patternType, warnings.Count),
Warnings = warnings,
};
}
private static string ResolvePatternType(IReadOnlyList cluster)
{
return cluster.Count switch
{
1 => "single",
2 => "line_2",
3 => "triangle_3",
4 when IsRectangle(cluster) => "rectangle_4",
_ => "generic",
};
}
private static int ResolveMinRequiredHoleCount(string patternType, int pointCount, bool isSymmetricRectangle)
{
if (string.Equals(patternType, "single", StringComparison.OrdinalIgnoreCase))
{
return 1;
}
if (string.Equals(patternType, "line_2", StringComparison.OrdinalIgnoreCase))
{
return 2;
}
if (string.Equals(patternType, "triangle_3", StringComparison.OrdinalIgnoreCase))
{
return 3;
}
if (isSymmetricRectangle)
{
return 2;
}
if (pointCount <= 0)
{
return 1;
}
return pointCount <= 4 ? pointCount : Math.Min(pointCount, 4);
}
private static bool IsRectangle(IReadOnlyList cluster)
{
if (cluster.Count != 4)
{
return false;
}
var distances = new List();
for (var i = 0; i < cluster.Count; i++)
{
for (var j = i + 1; j < cluster.Count; j++)
{
distances.Add(Distance(cluster[i].Position, cluster[j].Position));
}
}
distances.Sort();
if (distances.Count != 6)
{
return false;
}
var sideA = distances[0];
var sideB = distances[2];
var diagonalA = distances[4];
var diagonalB = distances[5];
return AreClose(sideA, distances[1], 0.12) &&
AreClose(sideB, distances[3], 0.12) &&
AreClose(diagonalA, diagonalB, 0.08);
}
private static double[] AveragePosition(IReadOnlyList cluster)
{
if (cluster.Count == 0)
{
return new[] { 0.0, 0.0, 0.0 };
}
return new[]
{
Math.Round(cluster.Average(item => item.Position[0]), 2),
Math.Round(cluster.Average(item => item.Position[1]), 2),
Math.Round(cluster.Average(item => item.Position[2]), 2),
};
}
private static double[] ResolveMountFaceNormal(string mountFace)
{
return mountFace.ToLowerInvariant() switch
{
"left" => new[] { -1.0, 0.0, 0.0 },
"right" => new[] { 1.0, 0.0, 0.0 },
"front" => new[] { 0.0, -1.0, 0.0 },
"back" => new[] { 0.0, 1.0, 0.0 },
"bottom" => new[] { 0.0, 0.0, -1.0 },
"top" => new[] { 0.0, 0.0, 1.0 },
_ => new[] { 0.0, 0.0, 1.0 },
};
}
private static double ComputePatternConfidence(
IReadOnlyList cluster,
string patternType,
int warningCount)
{
if (cluster.Count == 0)
{
return 0;
}
var baseConfidence = cluster.Average(item => item.Confidence);
if (string.Equals(patternType, "generic", StringComparison.OrdinalIgnoreCase))
{
baseConfidence = Math.Min(baseConfidence, 0.68);
}
if (warningCount > 0)
{
baseConfidence -= Math.Min(0.14, warningCount * 0.04);
}
return Math.Round(Clamp01(baseConfidence), 4);
}
private static string[] NormalizeMountFaces(IEnumerable? faces, string? fallbackFace = null)
{
var normalized = (faces ?? Array.Empty())
.Where(face => !string.IsNullOrWhiteSpace(face))
.Select(face => face.Trim().ToLowerInvariant())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (normalized.Count > 0)
{
return normalized.ToArray();
}
if (!string.IsNullOrWhiteSpace(fallbackFace))
{
return new[] { fallbackFace!.Trim().ToLowerInvariant() };
}
return new[] { "unknown" };
}
private static double Distance(IReadOnlyList left, IReadOnlyList right)
{
var dx = left[0] - right[0];
var dy = left[1] - right[1];
var dz = left[2] - right[2];
return Math.Sqrt(dx * dx + dy * dy + dz * dz);
}
private static bool AreClose(double left, double right, double toleranceRatio)
{
var baseValue = Math.Max(Math.Abs(left), Math.Abs(right));
if (baseValue <= 1e-6)
{
return true;
}
return Math.Abs(left - right) / baseValue <= toleranceRatio;
}
private static double Clamp01(double value)
{
if (value < 0) return 0;
if (value > 1) return 1;
return value;
}
private sealed class MountPatternUnit
{
public string Id { get; set; } = "";
public string PanelId { get; set; } = "";
public string[] HoleIds { get; set; } = Array.Empty();
public double[] Position { get; set; } = new double[3];
public double[] Normal { get; set; } = new double[3];
public string? GroupType { get; set; }
public string[] MountFaces { get; set; } = Array.Empty();
public double Confidence { get; set; }
public List Warnings { get; set; } = new();
public string Source { get; set; } = "single-hole";
}
}