WuYongJie 5821d73291 初始提交:3D家具安装展示系统
包含:
- furniture-3d-system:Next.js 16 全栈应用(资产中心、产品管理、3D装配动画)
- autocad-plugin:AutoCAD 家具导出插件源码
- 资产中心改造:编辑弹窗化、删除功能、Toast反馈、自定义Select组件

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 14:00:19 +08:00

717 lines
25 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;
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;
}
}
}