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 }; } }