442 lines
14 KiB
C#
Raw 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>
/// 安装图样识别器。
///
/// 设计口径:
/// - 吸附面(mount face)先缩小候选范围;
/// - HoleGroup 负责把几何孔组织成业务孔位;
/// - MountPattern 负责定义“至少命中几个孔才允许吸附成立”。
/// </summary>
public static class MountPatternAnalyzer
{
private const double DefaultClusterDistanceMm = 220.0;
public static List<MountPatternInfo> BuildMountPatterns(
IReadOnlyList<PanelInfo> panels,
IReadOnlyList<HoleInfo> holes,
IReadOnlyList<HoleGroupInfo> holeGroups)
{
var patterns = new List<MountPatternInfo>();
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<HoleInfo>();
var panelGroups = groupsByPanel.TryGetValue(panel.Id, out var panelGroupList)
? panelGroupList
: new List<HoleGroupInfo>();
var units = BuildUnits(panel.Id, panelHoles, panelGroups);
if (units.Count == 0)
{
panel.MountPatterns = new List<MountPatternInfo>();
continue;
}
var panelPatterns = new List<MountPatternInfo>();
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;
patterns.AddRange(panelPatterns);
}
return patterns;
}
public static void AttachPatternsToPanels(IReadOnlyList<PanelInfo> panels, IReadOnlyList<MountPatternInfo> patterns)
{
var map = patterns
.GroupBy(pattern => pattern.PanelId, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.ToList(), StringComparer.OrdinalIgnoreCase);
foreach (var panel in panels)
{
panel.MountPatterns = map.TryGetValue(panel.Id, out var items)
? items
: new List<MountPatternInfo>();
}
}
private static List<MountPatternUnit> BuildUnits(
string panelId,
IReadOnlyList<HoleInfo> holes,
IReadOnlyList<HoleGroupInfo> holeGroups)
{
var units = new List<MountPatternUnit>();
var groupedHoleIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var group in holeGroups)
{
foreach (var holeId in group.HoleIds ?? Array.Empty<string>())
{
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<string>(),
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<string>(),
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<string>(),
Source = "single-hole",
});
}
return units;
}
private static List<List<MountPatternUnit>> BuildClusters(IReadOnlyList<MountPatternUnit> units)
{
var clusters = new List<List<MountPatternUnit>>();
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var unit in units)
{
if (!visited.Add(unit.Id))
{
continue;
}
var cluster = new List<MountPatternUnit>();
var queue = new Queue<MountPatternUnit>();
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<MountPatternUnit> 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<string>();
var optionalHoleIds = Array.Empty<string>();
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<MountPatternUnit> 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<MountPatternUnit> cluster)
{
if (cluster.Count != 4)
{
return false;
}
var distances = new List<double>();
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<MountPatternUnit> 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<MountPatternUnit> 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<string>? faces, string? fallbackFace = null)
{
var normalized = (faces ?? Array.Empty<string>())
.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<double> left, IReadOnlyList<double> 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<string>();
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<string>();
public double Confidence { get; set; }
public List<string> Warnings { get; set; } = new();
public string Source { get; set; } = "single-hole";
}
}