包含: - furniture-3d-system:Next.js 16 全栈应用(资产中心、产品管理、3D装配动画) - autocad-plugin:AutoCAD 家具导出插件源码 - 资产中心改造:编辑弹窗化、删除功能、Toast反馈、自定义Select组件 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
717 lines
25 KiB
C#
717 lines
25 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Text.RegularExpressions;
|
||
using Autodesk.AutoCAD.DatabaseServices;
|
||
using Autodesk.AutoCAD.Geometry;
|
||
|
||
namespace FurnitureExporter;
|
||
|
||
/// <summary>
|
||
/// 五金件分析器 - 提取五金件的位置和几何特征
|
||
/// v2.2: 增加图层名称识别和五金件类型推断
|
||
/// </summary>
|
||
public class HardwareAnalyzer
|
||
{
|
||
private readonly Database _db;
|
||
private Point3d _originOffset = Point3d.Origin;
|
||
|
||
// 几何特征匹配容差 (mm)
|
||
private const double SIZE_TOLERANCE = 0.5;
|
||
private const double VOLUME_TOLERANCE = 10;
|
||
|
||
// 关联判断参数
|
||
private const double HOLE_ASSOCIATION_TOLERANCE = 5; // 五金件与孔位关联容差 5mm
|
||
private const double PANEL_ASSOCIATION_TOLERANCE = 50; // 五金件与板材关联容差 50mm
|
||
|
||
// 图层名称到五金件类型的映射规则(按关键词匹配)
|
||
private static readonly Dictionary<string, string> LayerKeywordMapping = new(StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
// 铰链相关
|
||
{ "铰链", "hinge" },
|
||
{ "合页", "hinge" },
|
||
{ "门转轴", "hinge" },
|
||
{ "hinge", "hinge" },
|
||
|
||
// 螺丝相关
|
||
{ "螺丝", "screw" },
|
||
{ "螺钉", "screw" },
|
||
{ "自攻", "screw" },
|
||
{ "内六角", "screw" },
|
||
{ "screw", "screw" },
|
||
|
||
// 连接件相关
|
||
{ "三合一", "connector" },
|
||
{ "偏心", "connector" },
|
||
{ "连接件", "connector" },
|
||
{ "connector", "connector" },
|
||
|
||
// 层板托相关
|
||
{ "层板托", "shelf_pin" },
|
||
{ "搁板销", "shelf_pin" },
|
||
{ "托", "shelf_pin" },
|
||
|
||
// 抽屉滑轨相关
|
||
{ "滑轨", "drawer_slide" },
|
||
{ "导轨", "drawer_slide" },
|
||
{ "抽屉", "drawer_slide" },
|
||
{ "slide", "drawer_slide" },
|
||
|
||
// 门吸/反弹器
|
||
{ "反弹", "push_latch" },
|
||
{ "门吸", "magnetic_catch" },
|
||
{ "吸铁", "magnetic_catch" },
|
||
|
||
// 拉手相关
|
||
{ "拉手", "handle" },
|
||
{ "把手", "handle" },
|
||
{ "handle", "handle" },
|
||
|
||
// 孔/透气孔
|
||
{ "透气孔", "vent" },
|
||
{ "过线孔", "cable_hole" },
|
||
{ "走线孔", "cable_hole" },
|
||
};
|
||
|
||
public HardwareAnalyzer(Database db)
|
||
{
|
||
_db = db;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据图层名称推断五金件类型
|
||
/// </summary>
|
||
public static string InferHardwareTypeFromLayer(string layerName)
|
||
{
|
||
if (string.IsNullOrEmpty(layerName))
|
||
return "unknown";
|
||
|
||
foreach (var kvp in LayerKeywordMapping)
|
||
{
|
||
if (layerName.IndexOf(kvp.Key, StringComparison.OrdinalIgnoreCase) >= 0)
|
||
{
|
||
return kvp.Value;
|
||
}
|
||
}
|
||
|
||
return "unknown";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分析五金件,提取位置和几何特征
|
||
/// </summary>
|
||
/// <param name="hardwareIds">五金件实体 ID 数组</param>
|
||
/// <param name="originOffset">原点偏移(可选,用于将坐标归一化到世界原点)</param>
|
||
public ExportData Analyze(ObjectId[] hardwareIds, Point3d? originOffset = null)
|
||
{
|
||
_originOffset = originOffset ?? Point3d.Origin;
|
||
|
||
var exportData = new ExportData
|
||
{
|
||
HardwareTypes = new List<HardwareType>()
|
||
};
|
||
|
||
if (hardwareIds.Length == 0)
|
||
return exportData;
|
||
|
||
using var tr = _db.TransactionManager.StartTransaction();
|
||
|
||
// 收集所有五金件的特征和位置
|
||
var instances = new List<(GeometrySignature Signature, HardwareInstance Instance)>();
|
||
|
||
foreach (var id in hardwareIds)
|
||
{
|
||
var ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
|
||
if (ent == null) continue;
|
||
|
||
var result = ExtractHardwareInfo(ent);
|
||
if (result != null)
|
||
{
|
||
instances.Add(result.Value);
|
||
}
|
||
}
|
||
|
||
// 按几何特征分组
|
||
var groups = GroupBySignature(instances);
|
||
|
||
// 转换为导出格式
|
||
int typeIndex = 0;
|
||
foreach (var group in groups)
|
||
{
|
||
var hwType = new HardwareType
|
||
{
|
||
TypeId = $"type_{typeIndex++:D3}",
|
||
Signature = group.Key,
|
||
Instances = group.Value
|
||
};
|
||
exportData.HardwareTypes.Add(hwType);
|
||
}
|
||
|
||
tr.Commit();
|
||
return exportData;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分析五金件并关联板材和孔位(v2.2 增强版)
|
||
/// </summary>
|
||
/// <param name="hardwareIds">五金件实体 ID 数组</param>
|
||
/// <param name="panels">板材信息列表</param>
|
||
/// <param name="holes">孔位信息列表</param>
|
||
/// <param name="originOffset">原点偏移</param>
|
||
public ExportData AnalyzeWithAssociation(
|
||
ObjectId[] hardwareIds,
|
||
List<PanelInfo> panels,
|
||
List<HoleInfo> holes,
|
||
Point3d? originOffset = null)
|
||
{
|
||
var exportData = Analyze(hardwareIds, originOffset);
|
||
|
||
// 为每个五金件实例添加关联信息
|
||
foreach (var hwType in exportData.HardwareTypes)
|
||
{
|
||
foreach (var instance in hwType.Instances)
|
||
{
|
||
AssociateWithPanelsAndHoles(instance, panels, holes);
|
||
}
|
||
|
||
// v2.2: 分析图层信息,推断五金件类型
|
||
var layerCounts = hwType.Instances
|
||
.Where(i => !string.IsNullOrEmpty(i.SourceLayer))
|
||
.GroupBy(i => i.SourceLayer!)
|
||
.ToDictionary(g => g.Key, g => g.Count());
|
||
|
||
if (layerCounts.Count > 0)
|
||
{
|
||
// 取数量最多的图层作为主要图层
|
||
var primaryLayer = layerCounts.OrderByDescending(kvp => kvp.Value).First().Key;
|
||
hwType.PrimaryLayerName = primaryLayer;
|
||
hwType.InferredHardwareType = InferHardwareTypeFromLayer(primaryLayer);
|
||
}
|
||
}
|
||
|
||
// v2.2: 生成图层信息映射
|
||
GenerateLayerInfo(exportData, hardwareIds);
|
||
|
||
// v2.2: 生成五金件按图层汇总
|
||
GenerateHardwareSummary(exportData);
|
||
|
||
return exportData;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成图层信息映射(v2.2 新增)
|
||
/// </summary>
|
||
private void GenerateLayerInfo(ExportData exportData, ObjectId[] hardwareIds)
|
||
{
|
||
using var tr = _db.TransactionManager.StartTransaction();
|
||
|
||
var layerEntityCount = new Dictionary<string, int>();
|
||
|
||
foreach (var id in hardwareIds)
|
||
{
|
||
var ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
|
||
if (ent == null) continue;
|
||
|
||
var layerName = ent.Layer;
|
||
if (!layerEntityCount.ContainsKey(layerName))
|
||
layerEntityCount[layerName] = 0;
|
||
layerEntityCount[layerName]++;
|
||
}
|
||
|
||
foreach (var kvp in layerEntityCount)
|
||
{
|
||
var layerName = kvp.Key;
|
||
var hwType = InferHardwareTypeFromLayer(layerName);
|
||
|
||
exportData.Layers[layerName] = new LayerInfo
|
||
{
|
||
Name = layerName,
|
||
Category = hwType != "unknown" ? "hardware" : "other",
|
||
HardwareType = hwType != "unknown" ? hwType : null,
|
||
EntityCount = kvp.Value
|
||
};
|
||
}
|
||
|
||
tr.Commit();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成五金件按图层汇总(v2.2 新增)
|
||
/// </summary>
|
||
private void GenerateHardwareSummary(ExportData exportData)
|
||
{
|
||
var summaryDict = new Dictionary<string, HardwareLayerSummary>();
|
||
|
||
int instanceIndex = 0;
|
||
foreach (var hwType in exportData.HardwareTypes)
|
||
{
|
||
foreach (var instance in hwType.Instances)
|
||
{
|
||
var layerName = instance.SourceLayer ?? "未知图层";
|
||
var hwTypeName = InferHardwareTypeFromLayer(layerName);
|
||
var key = $"{layerName}|{hwTypeName}";
|
||
|
||
if (!summaryDict.ContainsKey(key))
|
||
{
|
||
summaryDict[key] = new HardwareLayerSummary
|
||
{
|
||
LayerName = layerName,
|
||
HardwareType = hwTypeName,
|
||
Count = 0,
|
||
InstanceIds = new List<string>()
|
||
};
|
||
}
|
||
|
||
summaryDict[key].Count++;
|
||
summaryDict[key].InstanceIds.Add($"{hwType.TypeId}_inst_{instanceIndex++}");
|
||
}
|
||
}
|
||
|
||
exportData.HardwareSummary = summaryDict.Values
|
||
.OrderBy(s => s.HardwareType)
|
||
.ThenBy(s => s.LayerName)
|
||
.ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 为五金件实例关联板材和孔位
|
||
/// </summary>
|
||
private void AssociateWithPanelsAndHoles(HardwareInstance instance, List<PanelInfo> panels, List<HoleInfo> holes)
|
||
{
|
||
var hwPosition = new Point3d(instance.Position.X, instance.Position.Y, instance.Position.Z);
|
||
|
||
var candidateHoles = holes
|
||
.Select((hole) => new
|
||
{
|
||
Hole = hole,
|
||
Distance = hwPosition.DistanceTo(new Point3d(hole.Position[0], hole.Position[1], hole.Position[2]))
|
||
})
|
||
.OrderBy(item => item.Distance)
|
||
.ToList();
|
||
|
||
var matchedHoles = candidateHoles
|
||
.Where(item => item.Distance <= HOLE_ASSOCIATION_TOLERANCE)
|
||
.ToList();
|
||
|
||
if (matchedHoles.Count > 0)
|
||
{
|
||
var groupedHoleMatch = matchedHoles
|
||
.Where(item => !string.IsNullOrWhiteSpace(item.Hole.HoleGroupId))
|
||
.GroupBy(item => item.Hole.HoleGroupId!, StringComparer.OrdinalIgnoreCase)
|
||
.OrderByDescending(group => group.Count())
|
||
.ThenBy(group => group.Min(item => item.Distance))
|
||
.FirstOrDefault();
|
||
|
||
if (groupedHoleMatch != null)
|
||
{
|
||
var representative = groupedHoleMatch
|
||
.OrderBy(item => item.Distance)
|
||
.First();
|
||
var groupHoles = holes
|
||
.Where(hole => string.Equals(hole.HoleGroupId, groupedHoleMatch.Key, StringComparison.OrdinalIgnoreCase))
|
||
.ToList();
|
||
|
||
instance.NearestHoleId = representative.Hole.Id;
|
||
instance.DistanceToNearestHole = Math.Round(representative.Distance, 2);
|
||
instance.PrimaryPanelId = representative.Hole.PanelId;
|
||
instance.SecondaryPanelId = FindSecondaryPanelId(representative.Hole.PanelId, hwPosition, panels);
|
||
instance.BoundHoleGroupId = groupedHoleMatch.Key;
|
||
instance.BoundHoleIds = groupHoles.Select(hole => hole.Id).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||
instance.BindingMode = "hole_group";
|
||
instance.BindingConfidence = Math.Round(ComputeHoleBindingConfidence(representative.Distance, groupHoles.Count), 4);
|
||
instance.BindingReason = $"优先按孔组 {groupedHoleMatch.Key} 绑定";
|
||
instance.NearbyPanelIds = BuildNearbyPanelIds(instance.PrimaryPanelId, instance.SecondaryPanelId, hwPosition, panels);
|
||
return;
|
||
}
|
||
|
||
var nearestHole = matchedHoles[0];
|
||
instance.NearestHoleId = nearestHole.Hole.Id;
|
||
instance.DistanceToNearestHole = Math.Round(nearestHole.Distance, 2);
|
||
instance.PrimaryPanelId = nearestHole.Hole.PanelId;
|
||
instance.SecondaryPanelId = FindSecondaryPanelId(nearestHole.Hole.PanelId, hwPosition, panels);
|
||
instance.BoundHoleIds = new[] { nearestHole.Hole.Id };
|
||
instance.BindingMode = "hole";
|
||
instance.BindingConfidence = Math.Round(ComputeHoleBindingConfidence(nearestHole.Distance, 1), 4);
|
||
instance.BindingReason = $"优先按单孔 {nearestHole.Hole.Id} 绑定";
|
||
instance.NearbyPanelIds = BuildNearbyPanelIds(instance.PrimaryPanelId, instance.SecondaryPanelId, hwPosition, panels);
|
||
return;
|
||
}
|
||
|
||
var nearbyPanels = FindNearbyPanels(hwPosition, panels);
|
||
if (nearbyPanels.Count > 0)
|
||
{
|
||
instance.PrimaryPanelId = nearbyPanels[0].Panel.Id;
|
||
instance.SecondaryPanelId = nearbyPanels.Count > 1 ? nearbyPanels[1].Panel.Id : null;
|
||
instance.NearbyPanelIds = nearbyPanels.Select(item => item.Panel.Id).Take(4).ToArray();
|
||
instance.BindingMode = nearbyPanels[0].UsedBoundingBox ? "panel_bbox" : "panel_center";
|
||
instance.BindingConfidence = Math.Round(ComputePanelBindingConfidence(nearbyPanels[0].Distance, nearbyPanels[0].UsedBoundingBox), 4);
|
||
instance.BindingReason = nearbyPanels[0].UsedBoundingBox
|
||
? "未命中孔位,按板件包围盒邻近绑定"
|
||
: "未命中孔位,按板件中心邻近绑定";
|
||
return;
|
||
}
|
||
|
||
instance.BindingMode = "none";
|
||
instance.BindingConfidence = 0;
|
||
instance.BindingReason = "未找到可关联的孔位或板件";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断点是否在扩展边界框内
|
||
/// </summary>
|
||
private bool IsPointInBox(Point3d point, Point3d min, Point3d max)
|
||
{
|
||
return point.X >= min.X && point.X <= max.X
|
||
&& point.Y >= min.Y && point.Y <= max.Y
|
||
&& point.Z >= min.Z && point.Z <= max.Z;
|
||
}
|
||
|
||
private List<(PanelInfo Panel, double Distance, bool UsedBoundingBox)> FindNearbyPanels(Point3d hwPosition, List<PanelInfo> panels)
|
||
{
|
||
var nearbyPanels = new List<(PanelInfo Panel, double Distance, bool UsedBoundingBox)>();
|
||
|
||
foreach (var panel in panels)
|
||
{
|
||
if (panel.BoundingBox != null)
|
||
{
|
||
var minPt = new Point3d(panel.BoundingBox.Min[0], panel.BoundingBox.Min[1], panel.BoundingBox.Min[2]);
|
||
var maxPt = new Point3d(panel.BoundingBox.Max[0], panel.BoundingBox.Max[1], panel.BoundingBox.Max[2]);
|
||
|
||
var expandedMin = new Point3d(
|
||
minPt.X - PANEL_ASSOCIATION_TOLERANCE,
|
||
minPt.Y - PANEL_ASSOCIATION_TOLERANCE,
|
||
minPt.Z - PANEL_ASSOCIATION_TOLERANCE
|
||
);
|
||
var expandedMax = new Point3d(
|
||
maxPt.X + PANEL_ASSOCIATION_TOLERANCE,
|
||
maxPt.Y + PANEL_ASSOCIATION_TOLERANCE,
|
||
maxPt.Z + PANEL_ASSOCIATION_TOLERANCE
|
||
);
|
||
|
||
if (IsPointInBox(hwPosition, expandedMin, expandedMax))
|
||
{
|
||
nearbyPanels.Add((panel, DistanceToExpandedBox(hwPosition, expandedMin, expandedMax), true));
|
||
}
|
||
continue;
|
||
}
|
||
|
||
var panelCenter = new Point3d(panel.Position[0], panel.Position[1], panel.Position[2]);
|
||
var distance = hwPosition.DistanceTo(panelCenter);
|
||
var maxPanelDim = Math.Max(panel.Size[0], Math.Max(panel.Size[1], panel.Size[2]));
|
||
|
||
if (distance < maxPanelDim / 2 + PANEL_ASSOCIATION_TOLERANCE)
|
||
{
|
||
nearbyPanels.Add((panel, distance, false));
|
||
}
|
||
}
|
||
|
||
return nearbyPanels
|
||
.OrderBy(item => item.Distance)
|
||
.ToList();
|
||
}
|
||
|
||
private string[]? BuildNearbyPanelIds(string? primaryPanelId, string? secondaryPanelId, Point3d hwPosition, List<PanelInfo> panels)
|
||
{
|
||
var ordered = new List<string>();
|
||
|
||
if (!string.IsNullOrWhiteSpace(primaryPanelId))
|
||
{
|
||
ordered.Add(primaryPanelId!);
|
||
}
|
||
|
||
if (!string.IsNullOrWhiteSpace(secondaryPanelId) &&
|
||
!ordered.Contains(secondaryPanelId, StringComparer.OrdinalIgnoreCase))
|
||
{
|
||
ordered.Add(secondaryPanelId!);
|
||
}
|
||
|
||
foreach (var panel in FindNearbyPanels(hwPosition, panels))
|
||
{
|
||
if (ordered.Contains(panel.Panel.Id, StringComparer.OrdinalIgnoreCase))
|
||
continue;
|
||
ordered.Add(panel.Panel.Id);
|
||
if (ordered.Count >= 4)
|
||
break;
|
||
}
|
||
|
||
return ordered.Count > 0 ? ordered.ToArray() : null;
|
||
}
|
||
|
||
private string? FindSecondaryPanelId(string primaryPanelId, Point3d hwPosition, List<PanelInfo> panels)
|
||
{
|
||
foreach (var candidate in FindNearbyPanels(hwPosition, panels))
|
||
{
|
||
if (string.Equals(candidate.Panel.Id, primaryPanelId, StringComparison.OrdinalIgnoreCase))
|
||
continue;
|
||
return candidate.Panel.Id;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static double ComputeHoleBindingConfidence(double distance, int holeCount)
|
||
{
|
||
var distanceFactor = Math.Max(0.0, 1.0 - distance / HOLE_ASSOCIATION_TOLERANCE);
|
||
var groupBonus = holeCount > 1 ? 0.03 : 0.0;
|
||
return Math.Min(0.99, 0.92 + distanceFactor * 0.05 + groupBonus);
|
||
}
|
||
|
||
private static double ComputePanelBindingConfidence(double distance, bool usedBoundingBox)
|
||
{
|
||
var distanceFactor = Math.Max(0.0, 1.0 - distance / PANEL_ASSOCIATION_TOLERANCE);
|
||
var baseConfidence = usedBoundingBox ? 0.58 : 0.45;
|
||
var range = usedBoundingBox ? 0.22 : 0.18;
|
||
return Math.Min(0.88, baseConfidence + distanceFactor * range);
|
||
}
|
||
|
||
private static double DistanceToExpandedBox(Point3d point, Point3d min, Point3d max)
|
||
{
|
||
double dx = 0, dy = 0, dz = 0;
|
||
|
||
if (point.X < min.X) dx = min.X - point.X;
|
||
else if (point.X > max.X) dx = point.X - max.X;
|
||
|
||
if (point.Y < min.Y) dy = min.Y - point.Y;
|
||
else if (point.Y > max.Y) dy = point.Y - max.Y;
|
||
|
||
if (point.Z < min.Z) dz = min.Z - point.Z;
|
||
else if (point.Z > max.Z) dz = point.Z - max.Z;
|
||
|
||
return Math.Sqrt(dx * dx + dy * dy + dz * dz);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 提取单个五金件的信息
|
||
/// </summary>
|
||
private (GeometrySignature Signature, HardwareInstance Instance)? ExtractHardwareInfo(Entity entity)
|
||
{
|
||
if (entity is Solid3d solid)
|
||
{
|
||
return ExtractFromSolid(solid);
|
||
}
|
||
else if (entity is BlockReference blockRef)
|
||
{
|
||
return ExtractFromBlockReference(blockRef);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从 3D Solid 提取信息
|
||
/// </summary>
|
||
private (GeometrySignature Signature, HardwareInstance Instance)? ExtractFromSolid(Solid3d solid)
|
||
{
|
||
try
|
||
{
|
||
var ext = solid.GeometricExtents;
|
||
var size = ext.MaxPoint - ext.MinPoint;
|
||
var center = ext.MinPoint + (size / 2);
|
||
|
||
// 应用原点偏移
|
||
var offsetCenter = new Point3d(
|
||
center.X + _originOffset.X,
|
||
center.Y + _originOffset.Y,
|
||
center.Z + _originOffset.Z
|
||
);
|
||
|
||
// 规范化尺寸(排序,使比较一致)
|
||
var dims = new[] { size.X, size.Y, size.Z }.OrderByDescending(d => d).ToArray();
|
||
|
||
// 获取体积
|
||
double volume = 0;
|
||
try
|
||
{
|
||
var mass = solid.MassProperties;
|
||
volume = mass.Volume;
|
||
}
|
||
catch { }
|
||
|
||
var signature = new GeometrySignature
|
||
{
|
||
Width = Math.Round(dims[0], 2),
|
||
Height = Math.Round(dims[1], 2),
|
||
Depth = Math.Round(dims[2], 2),
|
||
Volume = Math.Round(volume, 2)
|
||
};
|
||
|
||
var instance = new HardwareInstance
|
||
{
|
||
Position = new Position3D
|
||
{
|
||
X = Math.Round(offsetCenter.X, 2),
|
||
Y = Math.Round(offsetCenter.Y, 2),
|
||
Z = Math.Round(offsetCenter.Z, 2)
|
||
},
|
||
Rotation = ExtractRotation(solid),
|
||
SourceLayer = solid.Layer,
|
||
SourceHandle = solid.Handle.ToString()
|
||
};
|
||
|
||
return (signature, instance);
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从图块引用提取信息
|
||
/// </summary>
|
||
private (GeometrySignature Signature, HardwareInstance Instance)? ExtractFromBlockReference(BlockReference blockRef)
|
||
{
|
||
try
|
||
{
|
||
var ext = blockRef.GeometricExtents;
|
||
var size = ext.MaxPoint - ext.MinPoint;
|
||
|
||
// 应用原点偏移到位置
|
||
var offsetPosition = new Point3d(
|
||
blockRef.Position.X + _originOffset.X,
|
||
blockRef.Position.Y + _originOffset.Y,
|
||
blockRef.Position.Z + _originOffset.Z
|
||
);
|
||
|
||
// 规范化尺寸
|
||
var dims = new[] { size.X, size.Y, size.Z }.OrderByDescending(d => d).ToArray();
|
||
|
||
var signature = new GeometrySignature
|
||
{
|
||
Width = Math.Round(dims[0], 2),
|
||
Height = Math.Round(dims[1], 2),
|
||
Depth = Math.Round(dims[2], 2),
|
||
Volume = 0, // 图块无法直接获取体积
|
||
BlockName = blockRef.Name
|
||
};
|
||
|
||
var instance = new HardwareInstance
|
||
{
|
||
Position = new Position3D
|
||
{
|
||
X = Math.Round(offsetPosition.X, 2),
|
||
Y = Math.Round(offsetPosition.Y, 2),
|
||
Z = Math.Round(offsetPosition.Z, 2)
|
||
},
|
||
Rotation = new Rotation3D
|
||
{
|
||
X = Math.Round(blockRef.Rotation, 4),
|
||
Y = 0,
|
||
Z = 0
|
||
},
|
||
Scale = new Scale3D
|
||
{
|
||
X = Math.Round(blockRef.ScaleFactors.X, 4),
|
||
Y = Math.Round(blockRef.ScaleFactors.Y, 4),
|
||
Z = Math.Round(blockRef.ScaleFactors.Z, 4)
|
||
},
|
||
SourceLayer = blockRef.Layer,
|
||
SourceHandle = blockRef.Handle.ToString(),
|
||
SourceBlockName = blockRef.Name
|
||
};
|
||
|
||
return (signature, instance);
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 提取实体的旋转信息
|
||
/// </summary>
|
||
private Rotation3D ExtractRotation(Solid3d solid)
|
||
{
|
||
// 简化处理:从边界框方向推断旋转
|
||
// 实际应用中可能需要更复杂的分析
|
||
return new Rotation3D { X = 0, Y = 0, Z = 0 };
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按几何特征分组
|
||
/// </summary>
|
||
private Dictionary<GeometrySignature, List<HardwareInstance>> GroupBySignature(
|
||
List<(GeometrySignature Signature, HardwareInstance Instance)> instances)
|
||
{
|
||
var groups = new Dictionary<GeometrySignature, List<HardwareInstance>>(
|
||
new SignatureComparer(SIZE_TOLERANCE, VOLUME_TOLERANCE));
|
||
|
||
foreach (var (sig, inst) in instances)
|
||
{
|
||
// 查找已有的匹配组
|
||
GeometrySignature? matchedKey = null;
|
||
foreach (var key in groups.Keys)
|
||
{
|
||
if (SignaturesMatch(key, sig))
|
||
{
|
||
matchedKey = key;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (matchedKey != null)
|
||
{
|
||
groups[matchedKey].Add(inst);
|
||
}
|
||
else
|
||
{
|
||
groups[sig] = new List<HardwareInstance> { inst };
|
||
}
|
||
}
|
||
|
||
return groups;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断两个特征是否匹配
|
||
/// </summary>
|
||
private bool SignaturesMatch(GeometrySignature a, GeometrySignature b)
|
||
{
|
||
return Math.Abs(a.Width - b.Width) <= SIZE_TOLERANCE
|
||
&& Math.Abs(a.Height - b.Height) <= SIZE_TOLERANCE
|
||
&& Math.Abs(a.Depth - b.Depth) <= SIZE_TOLERANCE
|
||
&& (a.Volume == 0 || b.Volume == 0 || Math.Abs(a.Volume - b.Volume) <= VOLUME_TOLERANCE);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 几何特征比较器
|
||
/// </summary>
|
||
internal class SignatureComparer : IEqualityComparer<GeometrySignature>
|
||
{
|
||
private readonly double _sizeTol;
|
||
private readonly double _volTol;
|
||
|
||
public SignatureComparer(double sizeTolerance, double volumeTolerance)
|
||
{
|
||
_sizeTol = sizeTolerance;
|
||
_volTol = volumeTolerance;
|
||
}
|
||
|
||
public bool Equals(GeometrySignature? x, GeometrySignature? y)
|
||
{
|
||
if (x == null || y == null) return false;
|
||
|
||
return Math.Abs(x.Width - y.Width) <= _sizeTol
|
||
&& Math.Abs(x.Height - y.Height) <= _sizeTol
|
||
&& Math.Abs(x.Depth - y.Depth) <= _sizeTol;
|
||
}
|
||
|
||
public int GetHashCode(GeometrySignature obj)
|
||
{
|
||
// 使用粗略的哈希,让 Equals 做精确比较
|
||
var w = (int)(Math.Round(obj.Width / 5) * 5);
|
||
var h = (int)(Math.Round(obj.Height / 5) * 5);
|
||
var d = (int)(Math.Round(obj.Depth / 5) * 5);
|
||
unchecked
|
||
{
|
||
int hash = 17;
|
||
hash = hash * 31 + w;
|
||
hash = hash * 31 + h;
|
||
hash = hash * 31 + d;
|
||
return hash;
|
||
}
|
||
}
|
||
}
|