using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using Newtonsoft.Json;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;
using AcApp = Autodesk.AutoCAD.ApplicationServices.Application;
[assembly: CommandClass(typeof(FurnitureExporter.Commands))]
[assembly: ExtensionApplication(typeof(FurnitureExporter.FurnitureExporterApp))]
namespace FurnitureExporter;
///
/// 插件初始化类
///
public class FurnitureExporterApp : IExtensionApplication
{
private const string PluginVersion = "2.5.0";
public void Initialize()
{
var doc = AcApp.DocumentManager.MdiActiveDocument;
doc?.Editor.WriteMessage("\n");
doc?.Editor.WriteMessage("╔════════════════════════════════════════════╗\n");
doc?.Editor.WriteMessage($"║ 家具导出插件 v{PluginVersion} 已加载 ║\n");
doc?.Editor.WriteMessage("║ ║\n");
doc?.Editor.WriteMessage("║ 命令: ║\n");
doc?.Editor.WriteMessage("║ FEXPORT - 整套家具兼容导出 ║\n");
doc?.Editor.WriteMessage("║ FEXPORTASSET - 单资产导出到资产中心 ║\n");
doc?.Editor.WriteMessage("║ FINFO - 查看选中实体信息 ║\n");
doc?.Editor.WriteMessage("║ ║\n");
doc?.Editor.WriteMessage($"║ v{PluginVersion} 新功能: ║\n");
doc?.Editor.WriteMessage("║ · 新增资产中心单资产导出模式 ║\n");
doc?.Editor.WriteMessage("║ · 新增 assembly 装配语义导出层 ║\n");
doc?.Editor.WriteMessage("║ · 语义优先用孔位/五金/配合面推导 ║\n");
doc?.Editor.WriteMessage("║ · 安装顺序仍保留给 Web 端人工设定 ║\n");
doc?.Editor.WriteMessage("║ · 导出置信度与 warnings 便于人工复核 ║\n");
doc?.Editor.WriteMessage("║ · 不破坏现有 panels/holes/hardware 数据 ║\n");
doc?.Editor.WriteMessage("╚════════════════════════════════════════════╝\n");
}
public void Terminate()
{
}
}
///
/// AutoCAD 命令类
///
public class Commands
{
private const string PluginVersion = "2.5.0";
private sealed class SingleAssetExportMode
{
public string AssetType { get; set; } = "";
public string DisplayName { get; set; } = "";
public string FileSuffix { get; set; } = "";
public bool AnalyzePanels { get; set; }
public bool AnalyzeHardware { get; set; }
public string SelectionPrompt { get; set; } = "";
}
private sealed class PanelExportAnalysis
{
public List Panels { get; } = new();
public List Holes { get; } = new();
public List HoleGroups { get; } = new();
public List MountPatterns { get; } = new();
public int LogicalHoleCount { get; set; }
public List EdgeFeatures { get; } = new();
public List ExcludedHoles { get; } = new();
}
private static readonly IReadOnlyDictionary SingleAssetExportModes =
new Dictionary(StringComparer.OrdinalIgnoreCase)
{
["Board"] = new()
{
AssetType = "BOARD",
DisplayName = "板材",
FileSuffix = "board_asset",
AnalyzePanels = true,
AnalyzeHardware = false,
SelectionPrompt = "请选择单个【板材资产】的实体(建议 3D Solid,可多选),然后按 Enter 确认: ",
},
["Component"] = new()
{
AssetType = "COMPONENT",
DisplayName = "组件",
FileSuffix = "component_asset",
AnalyzePanels = true,
AnalyzeHardware = false,
SelectionPrompt = "请选择单个【组件资产】的实体(可选 3D Solid / 图块,可多选),然后按 Enter 确认: ",
},
["Hardware"] = new()
{
AssetType = "HARDWARE",
DisplayName = "五金",
FileSuffix = "hardware_asset",
AnalyzePanels = false,
AnalyzeHardware = true,
SelectionPrompt = "请选择单个【五金资产】的实体(可选 3D Solid / 图块,可多选),然后按 Enter 确认: ",
},
};
// 单位转换常量(转换到毫米的系数)
private static readonly Dictionary UnitInfo = new()
{
{ UnitsValue.Undefined, ("未定义", 1.0) },
{ UnitsValue.Inches, ("英寸", 25.4) },
{ UnitsValue.Feet, ("英尺", 304.8) },
{ UnitsValue.Miles, ("英里", 1609344.0) },
{ UnitsValue.Millimeters, ("毫米", 1.0) },
{ UnitsValue.Centimeters, ("厘米", 10.0) },
{ UnitsValue.Meters, ("米", 1000.0) },
{ UnitsValue.Kilometers, ("千米", 1000000.0) },
// { UnitsValue.Microinches, ("微英寸", 0.0000254) }, // AutoCAD 2024 不支持
{ UnitsValue.Mils, ("密耳", 0.0254) },
{ UnitsValue.Yards, ("码", 914.4) },
{ UnitsValue.Angstroms, ("埃", 0.0000001) },
{ UnitsValue.Nanometers, ("纳米", 0.000001) },
{ UnitsValue.Microns, ("微米", 0.001) },
{ UnitsValue.Decimeters, ("分米", 100.0) },
{ UnitsValue.Dekameters, ("十米", 10000.0) },
{ UnitsValue.Hectometers, ("百米", 100000.0) },
{ UnitsValue.Gigameters, ("千兆米", 1e12) },
};
///
/// 检测图纸单位,返回单位名称和转换系数
///
private (UnitsValue Unit, string UnitName, double ConversionFactor, bool IsValid) CheckDrawingUnits(Database db, Editor ed)
{
var insunits = db.Insunits;
if (!UnitInfo.TryGetValue(insunits, out var info))
{
return (insunits, "未知", 1.0, false);
}
return (insunits, info.Name, info.ToMM, true);
}
///
/// 计算所有实体的边界框和中心点
///
private (Point3d Center, Point3d Min, Point3d Max, Vector3d Size) CalculateBounds(ObjectId[] ids, Database db)
{
using var tr = db.TransactionManager.StartTransaction();
Point3d? minPoint = null;
Point3d? maxPoint = null;
foreach (var id in ids)
{
var ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
if (ent == null) continue;
try
{
var ext = ent.GeometricExtents;
if (minPoint == null || maxPoint == null)
{
minPoint = ext.MinPoint;
maxPoint = ext.MaxPoint;
}
else
{
var min = minPoint.Value;
var max = maxPoint.Value;
minPoint = new Point3d(
Math.Min(min.X, ext.MinPoint.X),
Math.Min(min.Y, ext.MinPoint.Y),
Math.Min(min.Z, ext.MinPoint.Z)
);
maxPoint = new Point3d(
Math.Max(max.X, ext.MaxPoint.X),
Math.Max(max.Y, ext.MaxPoint.Y),
Math.Max(max.Z, ext.MaxPoint.Z)
);
}
}
catch { }
}
tr.Commit();
if (minPoint == null || maxPoint == null)
{
return (Point3d.Origin, Point3d.Origin, Point3d.Origin, new Vector3d(0, 0, 0));
}
var size = maxPoint.Value - minPoint.Value;
var center = new Point3d(
(minPoint.Value.X + maxPoint.Value.X) / 2,
(minPoint.Value.Y + maxPoint.Value.Y) / 2,
minPoint.Value.Z // 底部 Z 坐标,用于将模型底部放在 Z=0
);
return (center, minPoint.Value, maxPoint.Value, new Vector3d(size.X, size.Y, size.Z));
}
private bool EnsureMillimeterDrawingOrConfirm(Database db, Editor ed, out string unitName)
{
var (unit, detectedUnitName, conversionFactor, _) = CheckDrawingUnits(db, ed);
unitName = detectedUnitName;
ed.WriteMessage("\n[单位检测]\n");
ed.WriteMessage("─────────────────────────────\n");
ed.WriteMessage($" 当前图纸单位: {unitName}\n");
if (unit == UnitsValue.Millimeters)
{
ed.WriteMessage(" ✓ 单位正确\n");
return true;
}
ed.WriteMessage("\n");
ed.WriteMessage("╔════════════════════════════════════════════╗\n");
ed.WriteMessage("║ ⚠️ 单位错误 ║\n");
ed.WriteMessage("╚════════════════════════════════════════════╝\n");
ed.WriteMessage($" 当前单位: {unitName}\n");
ed.WriteMessage(" 要求单位: 毫米 (Millimeters)\n");
ed.WriteMessage("\n");
ed.WriteMessage(" 请修改图纸单位后重新导出:\n");
ed.WriteMessage(" 1. 输入命令 UNITS 打开单位设置\n");
ed.WriteMessage(" 2. 将 '插入时的缩放单位' 改为 '毫米'\n");
ed.WriteMessage(" 3. 点击确定\n");
ed.WriteMessage(" 4. 重新运行导出命令\n");
ed.WriteMessage("\n");
var continueOpt = new PromptKeywordOptions("\n是否强制继续? (可能导致比例错误) [是(Y)/否(N)]", "Yes No");
continueOpt.Keywords.Default = "No";
var continueResult = ed.GetKeywords(continueOpt);
if (continueResult.Status != PromptStatus.OK || continueResult.StringResult != "Yes")
{
ed.WriteMessage("\n已取消导出,请修改单位后重试。\n");
return false;
}
ed.WriteMessage($"\n ⚠️ 强制继续,将使用转换系数: {conversionFactor}\n");
return true;
}
private static double[] BuildModelBounds(Point3d minPt, Point3d maxPt, Point3d originOffset)
{
return new[]
{
Math.Round(minPt.X + originOffset.X, 2),
Math.Round(minPt.Y + originOffset.Y, 2),
Math.Round(minPt.Z + originOffset.Z, 2),
Math.Round(maxPt.X + originOffset.X, 2),
Math.Round(maxPt.Y + originOffset.Y, 2),
Math.Round(maxPt.Z + originOffset.Z, 2),
};
}
private SingleAssetExportMode? PromptSingleAssetExportMode(Editor ed)
{
var modeOptions = new PromptKeywordOptions(
"\n选择单资产类型 [板材(B)/组件(C)/五金(H)]",
"Board Component Hardware"
);
modeOptions.Keywords.Default = "Board";
modeOptions.AllowNone = true;
var modeResult = ed.GetKeywords(modeOptions);
if (modeResult.Status == PromptStatus.Cancel)
{
return null;
}
var keyword = modeResult.Status == PromptStatus.OK
? modeResult.StringResult
: modeOptions.Keywords.Default;
return keyword != null && SingleAssetExportModes.TryGetValue(keyword, out var mode)
? mode
: null;
}
private PanelExportAnalysis AnalyzePanelExportData(
Database db,
ObjectId[] solidIds,
Point3d originOffset,
Point3d minPt,
Point3d maxPt)
{
var result = new PanelExportAnalysis();
if (solidIds.Length == 0)
{
return result;
}
var panelAnalyzer = new PanelAnalyzer(db);
var panels = panelAnalyzer.Analyze(solidIds, originOffset);
result.Panels.AddRange(panels);
var allHoles = new List();
foreach (var panel in panels)
{
allHoles.AddRange(panel.Holes);
}
var panelsWithoutDetectedHoles = panels
.Where(panel => panel.Holes.Count == 0)
.ToList();
if (panelsWithoutDetectedHoles.Count > 0)
{
var markerDetector = new HoleMarkerDetector(db);
markerDetector.DetectAndAttach(
panels,
originOffset,
minPt,
maxPt,
new HashSet(panelsWithoutDetectedHoles.Select(panel => panel.Id))
);
allHoles.Clear();
foreach (var panel in panels)
{
allHoles.AddRange(panel.Holes);
}
}
foreach (var hole in allHoles)
{
hole.InferredUsage = HoleDetector.InferHoleUsage(hole.Diameter);
}
var holeGroups = HoleGrouping.BuildEccentricScrewGroups(allHoles);
HoleGrouping.AttachGroupsToPanels(panels, holeGroups);
var mountPatterns = MountPatternAnalyzer.BuildMountPatterns(panels, allHoles, holeGroups);
MountPatternAnalyzer.AttachPatternsToPanels(panels, mountPatterns);
var logicalHoleCount = HoleGrouping.ComputeLogicalHoleCount(allHoles);
foreach (var hole in allHoles)
{
hole.SuggestedFasteners = FastenerGuesser.SuggestForHole(hole, topN: 2);
}
var allEdgeFeatures = new List();
var chamferDetector = new ChamferDetector(db);
for (int i = 0; i < solidIds.Length && i < panels.Count; i++)
{
var features = chamferDetector.DetectEdgeFeatures(solidIds[i], panels[i].Id, originOffset);
allEdgeFeatures.AddRange(features);
}
result.Holes.AddRange(allHoles);
result.HoleGroups.AddRange(holeGroups);
result.MountPatterns.AddRange(mountPatterns);
result.LogicalHoleCount = logicalHoleCount;
result.EdgeFeatures.AddRange(allEdgeFeatures);
result.ExcludedHoles.AddRange(panelAnalyzer.ExcludedHoles);
return result;
}
///
/// 主导出命令 - 导出板材 GLB + 五金件位置 JSON
///
[CommandMethod("FEXPORT")]
public void ExportFurniture()
{
var doc = AcApp.DocumentManager.MdiActiveDocument;
if (doc == null)
{
return;
}
var db = doc.Database;
var ed = doc.Editor;
try
{
var docName = string.IsNullOrWhiteSpace(doc.Name) ? "Untitled" : doc.Name;
var docKey = docName;
ed.WriteMessage("\n");
ed.WriteMessage("╔════════════════════════════════════════════╗\n");
ed.WriteMessage("║ 家具模型导出向导 ║\n");
ed.WriteMessage("╚════════════════════════════════════════════╝\n");
// ========== 检测图纸单位 ==========
ed.WriteMessage("\n[单位检测]\n");
ed.WriteMessage("─────────────────────────────\n");
var (unit, unitName, conversionFactor, isValidUnit) = CheckDrawingUnits(db, ed);
ed.WriteMessage($" 当前图纸单位: {unitName}\n");
// 单位必须是毫米
if (unit != UnitsValue.Millimeters)
{
ed.WriteMessage("\n");
ed.WriteMessage("╔════════════════════════════════════════════╗\n");
ed.WriteMessage("║ ⚠️ 单位错误 ║\n");
ed.WriteMessage("╚════════════════════════════════════════════╝\n");
ed.WriteMessage($" 当前单位: {unitName}\n");
ed.WriteMessage($" 要求单位: 毫米 (Millimeters)\n");
ed.WriteMessage("\n");
ed.WriteMessage(" 请修改图纸单位后重新导出:\n");
ed.WriteMessage(" 1. 输入命令 UNITS 打开单位设置\n");
ed.WriteMessage(" 2. 将 '插入时的缩放单位' 改为 '毫米'\n");
ed.WriteMessage(" 3. 点击确定\n");
ed.WriteMessage(" 4. 重新运行 FEXPORT 命令\n");
ed.WriteMessage("\n");
// 询问是否继续(仅供高级用户)
var continueOpt = new PromptKeywordOptions("\n是否强制继续? (可能导致比例错误) [是(Y)/否(N)]", "Yes No");
continueOpt.Keywords.Default = "No";
var continueResult = ed.GetKeywords(continueOpt);
if (continueResult.Status != PromptStatus.OK || continueResult.StringResult != "Yes")
{
ed.WriteMessage("\n已取消导出,请修改单位后重试。\n");
return;
}
ed.WriteMessage($"\n ⚠️ 强制继续,将使用转换系数: {conversionFactor}\n");
}
else
{
ed.WriteMessage(" ✓ 单位正确\n");
}
// ========== 导出向导 ==========
// 默认推荐手动两步:先板件、再五金,准确率最高,兼容既有习惯。
// 自动模式仅作为提效兜底:一次框选整套家具,插件自动分组板件/五金。
ObjectId[] panelIds;
ObjectId[] hardwareIds = Array.Empty();
bool usedAutoMode = false;
bool likelyMergedSingleSolid = false;
var pending = ExportSessionManager.Get(docKey);
if (pending != null)
{
ed.WriteMessage("\n[恢复导出会话]\n");
ed.WriteMessage("─────────────────────────────\n");
ed.WriteMessage($" 检测到未完成的板材选择({pending.PanelHandles.Count} 个)。\n");
panelIds = ResolveObjectIdsFromHandles(db, pending.PanelHandles);
panelIds = FilterSolids(panelIds, db);
if (panelIds.Length == 0)
{
ed.WriteMessage(" ⚠️ 无法恢复板材(可能图纸已变更/实体被删除)。已清除会话,请重新运行 FEXPORT。\n");
ExportSessionManager.Clear(docKey);
return;
}
if (panelIds.Length != pending.PanelHandles.Count)
{
ed.WriteMessage($" ⚠️ 部分板材无法恢复:恢复 {panelIds.Length}/{pending.PanelHandles.Count} 个,将继续导出。\n");
}
}
else
{
var modeOptions = new PromptKeywordOptions(
"\n选择导出方式 [手动(M)/自动(A)]",
"Manual Auto"
);
modeOptions.Keywords.Default = "Manual";
modeOptions.AllowNone = true;
var modeResult = ed.GetKeywords(modeOptions);
if (modeResult.Status == PromptStatus.Cancel)
{
ed.WriteMessage("\n已取消导出。\n");
return;
}
var useAutoMode = modeResult.Status == PromptStatus.OK && modeResult.StringResult == "Auto";
if (useAutoMode)
{
usedAutoMode = true;
ed.WriteMessage("\n[自动模式] 一次框选整套家具\n");
ed.WriteMessage("─────────────────────────────\n");
var allOptions = new PromptSelectionOptions
{
MessageForAdding = "请框选整套家具(板件 + 五金件),然后按 Enter 确认: "
};
var allResult = ed.GetSelection(allOptions);
if (allResult.Status != PromptStatus.OK)
{
ed.WriteMessage("\n已取消导出。\n");
return;
}
var splitResult = AutoSplitPanelsAndHardware(allResult.Value.GetObjectIds(), db);
panelIds = splitResult.PanelIds;
hardwareIds = splitResult.HardwareIds;
likelyMergedSingleSolid = splitResult.IsLikelyMergedSingleSolid;
if (panelIds.Length == 0)
{
ed.WriteMessage("\n错误: 自动模式未识别到板件,请改用手动模式选择板件。\n");
return;
}
ed.WriteMessage($" ✓ 自动识别板件: {panelIds.Length} 个\n");
ed.WriteMessage($" ✓ 自动识别五金件: {hardwareIds.Length} 个\n");
if (!string.IsNullOrWhiteSpace(splitResult.FallbackMessage))
{
ed.WriteMessage($" ⚠️ {splitResult.FallbackMessage}\n");
}
if (likelyMergedSingleSolid)
{
ed.WriteMessage(" ⚠️ 检测到疑似“单体并体模型”:Web 端无法做分件爆炸/分件材质,只能整体预览。\n");
}
}
else
{
// ========== 第 1 步:选择板材(手动模式) ==========
ed.WriteMessage("\n[第 1 步 / 共 2 步] 选择板材\n");
ed.WriteMessage("─────────────────────────────\n");
var panelOptions = new PromptSelectionOptions
{
MessageForAdding = "请框选所有【板材】实体,然后按 Enter 确认: "
};
var panelResult = ed.GetSelection(panelOptions);
if (panelResult.Status != PromptStatus.OK)
{
ed.WriteMessage("\n已取消导出。\n");
return;
}
panelIds = FilterSolids(panelResult.Value.GetObjectIds(), db);
if (panelIds.Length == 0)
{
ed.WriteMessage("\n错误: 未选中任何 3D 实体,请确保选择的是 3D Solid。\n");
return;
}
// 保存板材句柄到会话,允许用户在两步之间自由切图层/执行其他命令
var panelHandles = GetEntityHandles(db, panelIds);
ExportSessionManager.SetPanels(docKey, panelHandles);
ed.WriteMessage($" ✓ 已记录 {panelIds.Length} 个板材\n");
ed.WriteMessage("\n 你现在可以自由调整图层显示/冻结/隔离等操作。\n");
ed.WriteMessage(" 调整完成后,再次运行 FEXPORT 继续第 2 步(选五金并导出)。\n");
var proceedOpt = new PromptKeywordOptions(
"\n是否现在直接继续第 2 步? [继续(C)/稍后(R)]",
"Continue Later"
);
proceedOpt.Keywords.Default = "Later";
proceedOpt.AllowNone = true;
var proceedResult = ed.GetKeywords(proceedOpt);
if (proceedResult.Status != PromptStatus.OK || proceedResult.StringResult != "Continue")
{
ed.WriteMessage("\n已暂停导出。提示:再次运行 FEXPORT 继续;运行 FEXPORTRESET 可清除暂存。\n");
return;
}
}
}
if (!usedAutoMode)
{
// ========== 第 2 步:选择五金件(手动/恢复模式) ==========
ed.WriteMessage("\n[第 2 步 / 共 2 步] 选择五金件\n");
ed.WriteMessage("─────────────────────────────\n");
var hardwareOptions = new PromptSelectionOptions
{
MessageForAdding = "请框选所有【五金件】,然后按 Enter 确认 (如无五金件可直接 Enter): "
};
var hardwareResult = ed.GetSelection(hardwareOptions);
if (hardwareResult.Status == PromptStatus.OK)
{
hardwareIds = FilterSolidsAndBlocks(hardwareResult.Value.GetObjectIds(), db);
ed.WriteMessage($" ✓ 已选中 {hardwareIds.Length} 个五金件\n");
}
else
{
ed.WriteMessage(" ✓ 未选择五金件,将只导出板材模型\n");
}
}
// ========== 选择保存路径 ==========
ed.WriteMessage("\n");
var saveDialog = new Microsoft.Win32.SaveFileDialog
{
Title = "保存导出文件",
Filter = "ZIP 压缩包 (*.zip)|*.zip",
DefaultExt = ".zip",
FileName = Path.GetFileNameWithoutExtension(docName) + "_export"
};
if (saveDialog.ShowDialog() != true)
{
ed.WriteMessage("已取消导出。\n");
return;
}
var exportPath = saveDialog.FileName;
var tempDir = Path.Combine(Path.GetTempPath(), $"furniture_export_{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
try
{
ed.WriteMessage("正在处理...\n");
ed.WriteMessage("─────────────────────────────\n");
// 计算所有实体的边界框(板材 + 五金件)
var allIds = panelIds.Concat(hardwareIds).ToArray();
var (center, minPt, maxPt, size) = CalculateBounds(allIds, db);
ed.WriteMessage($" 模型边界: {size.X:F1} × {size.Y:F1} × {size.Z:F1} mm\n");
ed.WriteMessage($" 原始中心: ({center.X:F1}, {center.Y:F1}, {center.Z:F1})\n");
// 计算原点偏移(将模型移动到世界原点,底部放在 Z=0)
var originOffset = new Point3d(-center.X, -center.Y, -minPt.Z);
ed.WriteMessage($" 原点偏移: ({originOffset.X:F1}, {originOffset.Y:F1}, {originOffset.Z:F1})\n");
ed.WriteMessage("\n");
// 1. 导出板材 OBJ 模型(带原点偏移,使用 Brep API 提取真实几何)
ed.WriteMessage(" [1/7] 导出板材模型 (真实几何)...");
var objPath = Path.Combine(tempDir, "model.obj");
var (meshDensity, maxSubdivisions) = RecommendMeshSettings(size, panelIds.Length);
ObjExporter.Export(panelIds, objPath, originOffset, meshDensity: meshDensity, maxSubdivisions: maxSubdivisions);
var objSize = new FileInfo(objPath).Length / 1024.0;
ed.WriteMessage($" ✓ ({objSize:F1} KB)\n");
ed.WriteMessage($" 网格参数: density={meshDensity:F0}, maxSubdivisions={maxSubdivisions}\n");
// 2. 分析板材信息(v2.0 新增)
ed.WriteMessage(" [2/7] 分析板材信息...");
var panelAnalyzer = new PanelAnalyzer(db);
var panels = panelAnalyzer.Analyze(panelIds, originOffset);
ed.WriteMessage($" ✓ ({panels.Count} 个板材)\n");
// 3. 提取孔位信息(v2.0 新增)
// 优先:Brep 真孔(布尔运算形成的圆柱面)
// Fallback:图纸标注孔(Circle/Block 内 Circle)
ed.WriteMessage(" [3/7] 提取孔位信息...");
var allHoles = new List();
foreach (var panel in panels)
{
allHoles.AddRange(panel.Holes);
}
var panelsWithoutDetectedHoles = panels
.Where(panel => panel.Holes.Count == 0)
.ToList();
if (panelsWithoutDetectedHoles.Count > 0)
{
// 标注孔只作为补充路径:
// - 优先保留 Brep 真孔
// - 仅为“当前未提取到实体孔”的板件补充标注孔
var markerDetector = new HoleMarkerDetector(db);
markerDetector.DetectAndAttach(
panels,
originOffset,
minPt,
maxPt,
new HashSet(panelsWithoutDetectedHoles.Select(panel => panel.Id))
);
allHoles.Clear();
foreach (var panel in panels)
{
allHoles.AddRange(panel.Holes);
}
}
// 1) 为每个孔推断用途(根据直径)
foreach (var hole in allHoles)
{
hole.InferredUsage = HoleDetector.InferHoleUsage(hole.Diameter);
}
// 2) 偏心孔位分组:把“主孔+侧孔”合并为 1 个逻辑孔位(不改变 holes 的几何数据,仅提供分组与逻辑计数)
var holeGroups = HoleGrouping.BuildEccentricScrewGroups(allHoles);
HoleGrouping.AttachGroupsToPanels(panels, holeGroups);
var mountPatterns = MountPatternAnalyzer.BuildMountPatterns(panels, allHoles, holeGroups);
MountPatternAnalyzer.AttachPatternsToPanels(panels, mountPatterns);
var logicalHoleCount = HoleGrouping.ComputeLogicalHoleCount(allHoles);
// 3) v2.3:根据孔的几何特征给出“更可能的螺丝直径”候选,避免硬匹配 4/5/6
foreach (var hole in allHoles)
{
hole.SuggestedFasteners = FastenerGuesser.SuggestForHole(hole, topN: 2);
}
ed.WriteMessage($" ✓ (几何孔 {allHoles.Count} 个, 逻辑孔 {logicalHoleCount} 个, 分组 {holeGroups.Count} 组)\n");
// 4. 检测倒角和圆角(v2.1 新增)
ed.WriteMessage(" [4/7] 检测边缘特征...");
var allEdgeFeatures = new List();
var chamferDetector = new ChamferDetector(db);
for (int i = 0; i < panelIds.Length && i < panels.Count; i++)
{
var features = chamferDetector.DetectEdgeFeatures(panelIds[i], panels[i].Id, originOffset);
allEdgeFeatures.AddRange(features);
}
var filletCount = allEdgeFeatures.Count(f => f.Type == "fillet");
var chamferCount = allEdgeFeatures.Count(f => f.Type == "chamfer");
ed.WriteMessage($" ✓ ({filletCount} 个圆角, {chamferCount} 个倒角)\n");
// 5. 分析五金件并关联板材和孔位(v2.0 增强)
ed.WriteMessage(" [5/7] 分析五金件关联...");
var hardwareAnalyzer = new HardwareAnalyzer(db);
var exportData = hardwareAnalyzer.AnalyzeWithAssociation(hardwareIds, panels, allHoles, originOffset);
exportData.ModelFile = "model.obj";
exportData.ExportedAt = DateTime.UtcNow.ToString("o");
exportData.SourceFile = Path.GetFileName(doc.Name);
exportData.Software = "AutoCAD 2024";
exportData.PluginVersion = PluginVersion;
exportData.PanelCount = panelIds.Length;
exportData.ExportMode = "product_bundle";
exportData.SourceEntityCount = allIds.Length;
// 添加单位和原点信息
exportData.Units = unitName;
exportData.OriginOffset = new double[] { originOffset.X, originOffset.Y, originOffset.Z };
exportData.ModelBounds = BuildModelBounds(minPt, maxPt, originOffset);
// 添加板材和孔位信息(v2.0 新增)
exportData.Panels = panels;
exportData.Holes = allHoles;
exportData.HoleGroups = holeGroups;
exportData.MountPatterns = mountPatterns;
exportData.LogicalHoleCount = logicalHoleCount;
if (panelAnalyzer.ExcludedHoles.Count > 0)
{
exportData.ExcludedHoles = panelAnalyzer.ExcludedHoles;
}
// 添加边缘特征信息(v2.1 新增)
exportData.EdgeFeatures = allEdgeFeatures;
var typeCount = exportData.HardwareTypes.Count;
var totalCount = exportData.HardwareTypes.Sum(t => t.Instances.Count);
var associatedCount = exportData.HardwareTypes
.SelectMany(t => t.Instances)
.Count(i => i.NearestHoleId != null || (i.NearbyPanelIds?.Length ?? 0) > 0);
ed.WriteMessage($" ✓ ({typeCount} 种类型, {totalCount} 个位置, {associatedCount} 个已关联)\n");
// 6. 分析板件装配语义(v2.4 新增)
ed.WriteMessage(" [6/7] 分析装配语义...");
var assemblyAnalyzer = new AssemblySemanticAnalyzer();
exportData.Assembly = assemblyAnalyzer.Analyze(
panels,
allHoles,
holeGroups,
exportData.HardwareTypes);
var assemblyPanelCount = exportData.Assembly.PanelAssemblies.Count;
var highConfidenceAssemblyCount = exportData.Assembly.PanelAssemblies.Count(item =>
item.Confidence >= 0.75 && !string.Equals(item.MountMode, "unknown", StringComparison.OrdinalIgnoreCase));
ed.WriteMessage($" ✓ ({assemblyPanelCount} 个板件语义, {highConfidenceAssemblyCount} 个高置信)\n");
// 7. 保存 JSON
ed.WriteMessage(" [7/7] 打包文件...");
var jsonPath = Path.Combine(tempDir, "hardware.json");
var jsonSettings = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore
};
var json = JsonConvert.SerializeObject(exportData, jsonSettings);
File.WriteAllText(jsonPath, json);
// 打包 ZIP
if (File.Exists(exportPath))
File.Delete(exportPath);
ZipFile.CreateFromDirectory(tempDir, exportPath);
var zipSize = new FileInfo(exportPath).Length / 1024.0;
ed.WriteMessage($" ✓\n");
// v2.2: 统计板件类型
var panelTypeCounts = panels
.Where(p => !string.IsNullOrEmpty(p.PanelType))
.GroupBy(p => p.PanelType)
.ToDictionary(g => g.Key!, g => g.Count());
// 完成
ed.WriteMessage("\n");
ed.WriteMessage("╔════════════════════════════════════════════╗\n");
ed.WriteMessage($"║ 导出完成 (v{PluginVersion}) ║\n");
ed.WriteMessage("╚════════════════════════════════════════════╝\n");
ed.WriteMessage($" 文件: {exportPath}\n");
ed.WriteMessage($" 大小: {zipSize:F1} KB\n");
ed.WriteMessage("\n");
ed.WriteMessage(" [板材统计]\n");
ed.WriteMessage($" 总数: {panelIds.Length} 个\n");
foreach (var kvp in panelTypeCounts.OrderByDescending(k => k.Value))
{
var typeName = GetPanelTypeName(kvp.Key);
ed.WriteMessage($" · {typeName}: {kvp.Value} 个\n");
}
ed.WriteMessage($" 孔位: {allHoles.Count} 个\n");
ed.WriteMessage($" 边缘特征: {allEdgeFeatures.Count} 个 ({filletCount} 圆角, {chamferCount} 倒角)\n");
ed.WriteMessage("\n");
ed.WriteMessage(" [五金件统计]\n");
ed.WriteMessage($" 总数: {totalCount} 个 ({typeCount} 种几何类型)\n");
// v2.2: 按图层显示五金件统计
foreach (var summary in exportData.HardwareSummary.Take(10))
{
var hwTypeName = GetHardwareTypeName(summary.HardwareType);
ed.WriteMessage($" · {summary.LayerName}: {summary.Count} 个 ({hwTypeName})\n");
}
if (exportData.HardwareSummary.Count > 10)
{
ed.WriteMessage($" ... 还有 {exportData.HardwareSummary.Count - 10} 种\n");
}
ed.WriteMessage($" 已关联: {associatedCount} 个\n");
if (likelyMergedSingleSolid)
{
ed.WriteMessage(" ⚠️ 检测到单体并体模型:Web 端无法分件爆炸/分件材质,请优先使用分件实体导出。\n");
}
ed.WriteMessage("\n");
ed.WriteMessage(" 请将此 ZIP 文件上传到系统。\n");
}
finally
{
// 清理临时目录
if (Directory.Exists(tempDir))
Directory.Delete(tempDir, true);
}
// 导出完成,清理会话
ExportSessionManager.Clear(docKey);
}
catch (System.Exception ex)
{
ed.WriteMessage($"\n导出失败: {ex.Message}\n");
ed.WriteMessage($"详细信息: {ex.StackTrace}\n");
}
}
[CommandMethod("FEXPORTASSET")]
public void ExportSingleAsset()
{
var doc = AcApp.DocumentManager.MdiActiveDocument;
if (doc == null)
{
return;
}
var db = doc.Database;
var ed = doc.Editor;
try
{
var docName = string.IsNullOrWhiteSpace(doc.Name) ? "Untitled" : doc.Name;
ed.WriteMessage("\n");
ed.WriteMessage("╔════════════════════════════════════════════╗\n");
ed.WriteMessage("║ 资产中心单资产导出向导 ║\n");
ed.WriteMessage("╚════════════════════════════════════════════╝\n");
if (!EnsureMillimeterDrawingOrConfirm(db, ed, out var unitName))
{
return;
}
var mode = PromptSingleAssetExportMode(ed);
if (mode == null)
{
ed.WriteMessage("\n已取消导出。\n");
return;
}
ed.WriteMessage($"\n[单资产模式] {mode.DisplayName}\n");
ed.WriteMessage("─────────────────────────────\n");
if (mode.AssetType == "HARDWARE")
{
ed.WriteMessage(" 当前模式会优先保留五金规格和安装基准,不再按整套家具归属导出。\n");
}
else
{
ed.WriteMessage(" 当前模式会保留孔位 / 孔组语义,供后续自由装配使用。\n");
}
var selectionOptions = new PromptSelectionOptions
{
MessageForAdding = mode.SelectionPrompt
};
var selectionResult = ed.GetSelection(selectionOptions);
if (selectionResult.Status != PromptStatus.OK)
{
ed.WriteMessage("\n已取消导出。\n");
return;
}
var selectedIds = FilterSolidsAndBlocks(selectionResult.Value.GetObjectIds(), db);
if (selectedIds.Length == 0)
{
ed.WriteMessage("\n错误: 未选中可导出的 3D Solid 或图块引用。\n");
return;
}
var solidIds = FilterSolids(selectedIds, db);
if (mode.AnalyzePanels && solidIds.Length == 0)
{
ed.WriteMessage(" ⚠️ 当前选择未检测到可直接识别的 3D Solid。\n");
ed.WriteMessage(" 本次只能导出整体模型,无法提取孔位 / 孔组语义。\n");
}
else if (mode.AnalyzePanels && solidIds.Length < selectedIds.Length)
{
ed.WriteMessage(" ⚠️ 当前选择包含图块引用或非 Solid 实体。\n");
ed.WriteMessage(" 这部分会进入模型导出,但其内部孔位不会被识别。\n");
}
ed.WriteMessage("\n");
var saveDialog = new Microsoft.Win32.SaveFileDialog
{
Title = "保存单资产导出文件",
Filter = "ZIP 压缩包 (*.zip)|*.zip",
DefaultExt = ".zip",
FileName = Path.GetFileNameWithoutExtension(docName) + "_" + mode.FileSuffix
};
if (saveDialog.ShowDialog() != true)
{
ed.WriteMessage("已取消导出。\n");
return;
}
var exportPath = saveDialog.FileName;
var tempDir = Path.Combine(Path.GetTempPath(), $"furniture_asset_export_{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
try
{
ed.WriteMessage("正在处理...\n");
ed.WriteMessage("─────────────────────────────\n");
var (center, minPt, maxPt, size) = CalculateBounds(selectedIds, db);
var originOffset = new Point3d(-center.X, -center.Y, -minPt.Z);
ed.WriteMessage($" 模型边界: {size.X:F1} × {size.Y:F1} × {size.Z:F1} mm\n");
ed.WriteMessage($" 原点偏移: ({originOffset.X:F1}, {originOffset.Y:F1}, {originOffset.Z:F1})\n");
ed.WriteMessage("\n");
ed.WriteMessage(" [1/5] 导出资产模型...");
var objPath = Path.Combine(tempDir, "model.obj");
var (meshDensity, maxSubdivisions) = RecommendMeshSettings(size, Math.Max(selectedIds.Length, 1));
ObjExporter.ExportEntities(
selectedIds,
objPath,
db,
originOffset,
meshDensity: meshDensity,
maxSubdivisions: maxSubdivisions);
var objSize = new FileInfo(objPath).Length / 1024.0;
ed.WriteMessage($" ✓ ({objSize:F1} KB)\n");
var exportData = new ExportData
{
ExportedAt = DateTime.UtcNow.ToString("o"),
SourceFile = Path.GetFileName(doc.Name),
Software = "AutoCAD 2024",
PluginVersion = PluginVersion,
ModelFile = "model.obj",
PanelCount = 0,
Units = unitName,
OriginOffset = new[] { originOffset.X, originOffset.Y, originOffset.Z },
ModelBounds = BuildModelBounds(minPt, maxPt, originOffset),
ExportMode = "single_asset",
AssetTypeHint = mode.AssetType,
SourceEntityCount = selectedIds.Length,
};
PanelExportAnalysis panelAnalysis = new();
if (mode.AnalyzePanels && solidIds.Length > 0)
{
ed.WriteMessage(" [2/5] 提取板件与孔位语义...");
panelAnalysis = AnalyzePanelExportData(db, solidIds, originOffset, minPt, maxPt);
exportData.Panels = panelAnalysis.Panels;
exportData.Holes = panelAnalysis.Holes;
exportData.HoleGroups = panelAnalysis.HoleGroups;
exportData.MountPatterns = panelAnalysis.MountPatterns;
exportData.LogicalHoleCount = panelAnalysis.LogicalHoleCount;
exportData.EdgeFeatures = panelAnalysis.EdgeFeatures;
exportData.PanelCount = panelAnalysis.Panels.Count;
if (panelAnalysis.ExcludedHoles.Count > 0)
{
exportData.ExcludedHoles = panelAnalysis.ExcludedHoles;
}
ed.WriteMessage($" ✓ ({panelAnalysis.Panels.Count} 个板件, 几何孔 {panelAnalysis.Holes.Count} 个, 逻辑孔 {panelAnalysis.LogicalHoleCount} 个)\n");
}
else
{
ed.WriteMessage(" [2/5] 提取板件与孔位语义... ✓ (当前模式无需提取)\n");
}
if (mode.AnalyzeHardware)
{
ed.WriteMessage(" [3/5] 提取五金规格语义...");
var hardwareAnalyzer = new HardwareAnalyzer(db);
var hardwareExport = hardwareAnalyzer.AnalyzeWithAssociation(
selectedIds,
panelAnalysis.Panels,
panelAnalysis.Holes,
originOffset);
exportData.HardwareTypes = hardwareExport.HardwareTypes;
exportData.Layers = hardwareExport.Layers;
exportData.HardwareSummary = hardwareExport.HardwareSummary;
var hardwareTypeCount = exportData.HardwareTypes.Count;
var hardwareInstanceCount = exportData.HardwareTypes.Sum(item => item.Instances.Count);
ed.WriteMessage($" ✓ ({hardwareTypeCount} 种类型, {hardwareInstanceCount} 个实例)\n");
}
else
{
ed.WriteMessage(" [3/5] 提取五金规格语义... ✓ (当前模式无需提取)\n");
}
if (mode.AnalyzePanels && panelAnalysis.Panels.Count > 0)
{
ed.WriteMessage(" [4/5] 生成装配语义...");
var assemblyAnalyzer = new AssemblySemanticAnalyzer();
exportData.Assembly = assemblyAnalyzer.Analyze(
panelAnalysis.Panels,
panelAnalysis.Holes,
panelAnalysis.HoleGroups,
exportData.HardwareTypes);
var highConfidenceCount = exportData.Assembly.PanelAssemblies.Count(item =>
item.Confidence >= 0.75 &&
!string.Equals(item.MountMode, "unknown", StringComparison.OrdinalIgnoreCase));
ed.WriteMessage($" ✓ ({exportData.Assembly.PanelAssemblies.Count} 个板件语义, {highConfidenceCount} 个高置信)\n");
}
else
{
ed.WriteMessage(" [4/5] 生成装配语义... ✓ (当前模式无需生成)\n");
}
ed.WriteMessage(" [5/5] 打包文件...");
var jsonPath = Path.Combine(tempDir, "hardware.json");
var jsonSettings = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore
};
var json = JsonConvert.SerializeObject(exportData, jsonSettings);
File.WriteAllText(jsonPath, json);
if (File.Exists(exportPath))
{
File.Delete(exportPath);
}
ZipFile.CreateFromDirectory(tempDir, exportPath);
var zipSize = new FileInfo(exportPath).Length / 1024.0;
ed.WriteMessage(" ✓\n");
ed.WriteMessage("\n");
ed.WriteMessage("╔════════════════════════════════════════════╗\n");
ed.WriteMessage($"║ 单资产导出完成 ({mode.DisplayName}) ║\n");
ed.WriteMessage("╚════════════════════════════════════════════╝\n");
ed.WriteMessage($" 文件: {exportPath}\n");
ed.WriteMessage($" 大小: {zipSize:F1} KB\n");
ed.WriteMessage($" 类型: {mode.AssetType}\n");
ed.WriteMessage($" 源实体: {selectedIds.Length} 个\n");
if (mode.AnalyzePanels)
{
ed.WriteMessage($" 板件: {exportData.Panels.Count} 个\n");
ed.WriteMessage($" 孔位: {exportData.Holes.Count} 个\n");
ed.WriteMessage($" 孔组: {exportData.HoleGroups.Count} 组\n");
}
if (mode.AnalyzeHardware)
{
ed.WriteMessage($" 五金类型: {exportData.HardwareTypes.Count} 种\n");
ed.WriteMessage($" 五金实例: {exportData.HardwareTypes.Sum(item => item.Instances.Count)} 个\n");
}
ed.WriteMessage("\n");
ed.WriteMessage(" 该 ZIP 可直接用于资产中心 CAD 导入。\n");
}
finally
{
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, true);
}
}
}
catch (System.Exception ex)
{
ed.WriteMessage($"\n单资产导出失败: {ex.Message}\n");
ed.WriteMessage($"详细信息: {ex.StackTrace}\n");
}
}
[CommandMethod("FEXPORTRESET")]
public void ResetExportSession()
{
var doc = AcApp.DocumentManager.MdiActiveDocument;
if (doc == null)
{
return;
}
var ed = doc.Editor;
var docKey = string.IsNullOrWhiteSpace(doc.Name) ? "Untitled" : doc.Name;
ExportSessionManager.Clear(docKey);
ed.WriteMessage("\n已清除 FEXPORT 暂存会话。请重新运行 FEXPORT。\n");
}
private static List GetEntityHandles(Database db, ObjectId[] ids)
{
var handles = new List();
using var tr = db.TransactionManager.StartTransaction();
foreach (var id in ids)
{
if (id.IsNull) continue;
var ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
if (ent == null) continue;
handles.Add(ent.Handle.ToString());
}
tr.Commit();
return handles;
}
private static ObjectId[] ResolveObjectIdsFromHandles(Database db, IReadOnlyList handles)
{
var ids = new List();
foreach (var h in handles)
{
if (string.IsNullOrWhiteSpace(h)) continue;
try
{
var handle = new Handle(Convert.ToInt64(h, 16));
var id = db.GetObjectId(false, handle, 0);
if (!id.IsNull) ids.Add(id);
}
catch
{
// ignore single handle failure
}
}
return ids.ToArray();
}
///
/// 查看实体信息命令
///
[CommandMethod("FINFO")]
public void ShowEntityInfo()
{
var doc = AcApp.DocumentManager.MdiActiveDocument;
var db = doc.Database;
var ed = doc.Editor;
var options = new PromptSelectionOptions
{
MessageForAdding = "选择要查看信息的实体: ",
SingleOnly = true
};
var result = ed.GetSelection(options);
if (result.Status != PromptStatus.OK) return;
using var tr = db.TransactionManager.StartTransaction();
foreach (var id in result.Value.GetObjectIds())
{
var ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
if (ent == null) continue;
ed.WriteMessage("\n");
ed.WriteMessage("╔════════════════════════════════════════════╗\n");
ed.WriteMessage("║ 实体信息 ║\n");
ed.WriteMessage("╚════════════════════════════════════════════╝\n");
ed.WriteMessage($" 类型: {ent.GetType().Name}\n");
ed.WriteMessage($" 图层: {ent.Layer}\n");
ed.WriteMessage($" 句柄: {ent.Handle}\n");
if (ent is Solid3d solid)
{
var ext = solid.GeometricExtents;
var size = ext.MaxPoint - ext.MinPoint;
ed.WriteMessage($" 尺寸: {size.X:F2} × {size.Y:F2} × {size.Z:F2} mm\n");
try
{
var mass = solid.MassProperties;
ed.WriteMessage($" 体积: {mass.Volume:F2} mm³\n");
}
catch { }
}
else if (ent is BlockReference blkRef)
{
ed.WriteMessage($" 图块名: {blkRef.Name}\n");
ed.WriteMessage($" 位置: ({blkRef.Position.X:F2}, {blkRef.Position.Y:F2}, {blkRef.Position.Z:F2})\n");
ed.WriteMessage($" 缩放: ({blkRef.ScaleFactors.X:F2}, {blkRef.ScaleFactors.Y:F2}, {blkRef.ScaleFactors.Z:F2})\n");
}
}
tr.Commit();
}
///
/// 过滤出 3D Solid 实体
///
private ObjectId[] FilterSolids(ObjectId[] ids, Database db)
{
var solids = new List();
using var tr = db.TransactionManager.StartTransaction();
foreach (var id in ids)
{
var ent = tr.GetObject(id, OpenMode.ForRead);
if (ent is Solid3d)
{
solids.Add(id);
}
}
tr.Commit();
return solids.ToArray();
}
///
/// 过滤出 3D Solid 和 BlockReference 实体
///
private ObjectId[] FilterSolidsAndBlocks(ObjectId[] ids, Database db)
{
var entities = new List();
using var tr = db.TransactionManager.StartTransaction();
foreach (var id in ids)
{
var ent = tr.GetObject(id, OpenMode.ForRead);
if (ent is Solid3d || ent is BlockReference)
{
entities.Add(id);
}
}
tr.Commit();
return entities.ToArray();
}
private sealed class AutoSplitResult
{
public ObjectId[] PanelIds { get; set; } = Array.Empty();
public ObjectId[] HardwareIds { get; set; } = Array.Empty();
public bool IsLikelyMergedSingleSolid { get; set; }
public string? FallbackMessage { get; set; }
}
private static readonly string[] PanelLayerKeywords =
{
"panel", "board", "carcass", "shelf", "divider", "side", "top", "bottom", "back",
"板", "板件", "柜体", "层板", "隔板", "侧板", "顶板", "底板", "背板"
};
private static readonly string[] HardwareLayerKeywords =
{
"hardware", "hinge", "screw", "connector", "slide", "handle", "leg", "bracket",
"五金", "铰链", "螺丝", "连接", "滑轨", "拉手", "柜脚", "角码", "合页"
};
private static (double MeshDensity, int MaxSubdivisions) RecommendMeshSettings(Vector3d size, int panelCount)
{
var longestEdge = Math.Max(size.X, Math.Max(size.Y, size.Z));
double meshDensity;
if (longestEdge >= 2400)
{
meshDensity = 28;
}
else if (longestEdge >= 1200)
{
meshDensity = 34;
}
else if (longestEdge >= 600)
{
meshDensity = 40;
}
else
{
meshDensity = 48;
}
if (panelCount <= 4)
{
meshDensity += 4;
}
else if (panelCount >= 20)
{
meshDensity -= 4;
}
meshDensity = Math.Max(24, Math.Min(60, meshDensity));
int maxSubdivisions;
if (panelCount <= 4)
{
maxSubdivisions = 12000;
}
else if (panelCount <= 12)
{
maxSubdivisions = 18000;
}
else
{
maxSubdivisions = 24000;
}
return (meshDensity, maxSubdivisions);
}
///
/// 自动模式:一次选择后,尽量自动分组板件与五金件,减少人工操作。
///
private AutoSplitResult AutoSplitPanelsAndHardware(ObjectId[] ids, Database db)
{
var panelSet = new HashSet();
var hardwareSet = new HashSet();
var allSolidIds = new List();
string? fallbackMessage = null;
double singleLength = 0;
double singleWidth = 0;
double singleThickness = 0;
using var tr = db.TransactionManager.StartTransaction();
foreach (var id in ids)
{
var ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
if (ent == null)
{
continue;
}
if (ent is BlockReference)
{
hardwareSet.Add(id);
continue;
}
if (ent is not Solid3d solid)
{
continue;
}
allSolidIds.Add(id);
if (!TryGetSortedDimensions(solid, out var length, out var width, out var thickness))
{
panelSet.Add(id);
continue;
}
if (allSolidIds.Count == 1)
{
singleLength = length;
singleWidth = width;
singleThickness = thickness;
}
var layerName = (ent.Layer ?? string.Empty).ToLowerInvariant();
var panelByLayer = HasAnyKeyword(layerName, PanelLayerKeywords);
var hardwareByLayer = HasAnyKeyword(layerName, HardwareLayerKeywords);
var panelByShape = IsLikelyPanelSolid(length, width, thickness);
if (hardwareByLayer && !panelByLayer)
{
hardwareSet.Add(id);
continue;
}
if (panelByLayer && !hardwareByLayer)
{
panelSet.Add(id);
continue;
}
if (panelByShape)
{
panelSet.Add(id);
}
else
{
hardwareSet.Add(id);
}
}
tr.Commit();
if (panelSet.Count == 0 && allSolidIds.Count > 0)
{
foreach (var id in allSolidIds)
{
panelSet.Add(id);
}
fallbackMessage = "未识别到典型板件形态,已自动按“全部 3D 实体=板件”继续导出。";
}
// 同一个实体不能同时属于板件和五金件
hardwareSet.RemoveWhere(panelSet.Contains);
var likelyMergedSingleSolid = false;
if (allSolidIds.Count == 1 && panelSet.Count == 1)
{
var thicknessTooLarge = singleThickness > 80;
var ratioA = singleLength / Math.Max(singleThickness, 1);
var ratioB = singleWidth / Math.Max(singleThickness, 1);
var ratioTooSmall = ratioA < 2.5 || ratioB < 1.6;
likelyMergedSingleSolid = thicknessTooLarge || ratioTooSmall;
}
return new AutoSplitResult
{
PanelIds = panelSet.ToArray(),
HardwareIds = hardwareSet.ToArray(),
IsLikelyMergedSingleSolid = likelyMergedSingleSolid,
FallbackMessage = fallbackMessage,
};
}
private static bool HasAnyKeyword(string text, IEnumerable keywords)
{
foreach (var keyword in keywords)
{
if (text.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
}
return false;
}
private static bool IsLikelyPanelSolid(double length, double width, double thickness)
{
if (thickness <= 0)
{
return false;
}
if (thickness < 4 || thickness > 80)
{
return false;
}
if (length < 80 || width < 60)
{
return false;
}
var ratioLength = length / thickness;
var ratioWidth = width / thickness;
return ratioLength >= 3.0 && ratioWidth >= 2.0;
}
private static bool TryGetSortedDimensions(Solid3d solid, out double length, out double width, out double thickness)
{
try
{
var ext = solid.GeometricExtents;
var size = ext.MaxPoint - ext.MinPoint;
var dims = new[]
{
Math.Abs(size.X),
Math.Abs(size.Y),
Math.Abs(size.Z),
}.OrderByDescending(d => d).ToArray();
length = dims[0];
width = dims[1];
thickness = dims[2];
return true;
}
catch
{
length = 0;
width = 0;
thickness = 0;
return false;
}
}
///
/// 获取板件类型的中文名称
///
private static string GetPanelTypeName(string panelType)
{
return panelType switch
{
"side_panel" => "侧板",
"top_panel" => "顶板",
"bottom_panel" => "底板",
"back_panel" => "背板",
"shelf" => "层板",
"vertical_divider" => "竖隔板",
"horizontal_divider" => "横隔板",
"panel" => "板件",
_ => panelType
};
}
///
/// 获取五金件类型的中文名称
///
private static string GetHardwareTypeName(string hardwareType)
{
return hardwareType switch
{
"hinge" => "铰链",
"screw" => "螺丝",
"connector" => "连接件",
"shelf_pin" => "层板托",
"drawer_slide" => "滑轨",
"push_latch" => "反弹器",
"magnetic_catch" => "门吸",
"handle" => "拉手",
"vent" => "透气孔",
"cable_hole" => "过线孔",
"unknown" => "未知",
_ => hardwareType
};
}
}