1557 lines
64 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.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;
/// <summary>
/// 插件初始化类
/// </summary>
public class FurnitureExporterApp : IExtensionApplication
{
private const string PluginVersion = "2.6.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("║ FEXPORTPRODUCT - 整套家具兼容导出 ║\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()
{
}
}
/// <summary>
/// AutoCAD 命令类
/// </summary>
public class Commands
{
private const string PluginVersion = "2.6.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<PanelInfo> Panels { get; } = new();
public List<HoleInfo> Holes { get; } = new();
public List<HoleGroupInfo> HoleGroups { get; } = new();
public List<MountPatternInfo> MountPatterns { get; } = new();
public List<MountPatternInfo> SnapPatterns { get; } = new();
public List<SnapSurfaceInfo> SnapSurfaces { get; } = new();
public int LogicalHoleCount { get; set; }
public List<EdgeFeatureInfo> EdgeFeatures { get; } = new();
public List<ExcludedHoleInfo> ExcludedHoles { get; } = new();
}
private static readonly IReadOnlyDictionary<string, SingleAssetExportMode> SingleAssetExportModes =
new Dictionary<string, SingleAssetExportMode>(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<UnitsValue, (string Name, double ToMM)> 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) },
};
/// <summary>
/// 检测图纸单位,返回单位名称和转换系数
/// </summary>
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);
}
/// <summary>
/// 计算所有实体的边界框和中心点
/// </summary>
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<HoleInfo>();
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<string>(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 snapSurfaces = SnapSurfaceBuilder.BuildSnapSurfaces(panels);
var logicalHoleCount = HoleGrouping.ComputeLogicalHoleCount(allHoles);
foreach (var hole in allHoles)
{
hole.SuggestedFasteners = FastenerGuesser.SuggestForHole(hole, topN: 2);
}
var allEdgeFeatures = new List<EdgeFeatureInfo>();
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.SnapPatterns.AddRange(mountPatterns);
result.SnapSurfaces.AddRange(snapSurfaces);
result.LogicalHoleCount = logicalHoleCount;
result.EdgeFeatures.AddRange(allEdgeFeatures);
result.ExcludedHoles.AddRange(panelAnalyzer.ExcludedHoles);
return result;
}
/// <summary>
/// 主导出命令 - 导出板材 GLB + 五金件位置 JSON
/// </summary>
[CommandMethod("FEXPORTPRODUCT")]
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. 重新运行 FEXPORTPRODUCT 命令\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<ObjectId>();
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(" ⚠️ 无法恢复板材(可能图纸已变更/实体被删除)。已清除会话,请重新运行 FEXPORTPRODUCT。\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(" 调整完成后,再次运行 FEXPORTPRODUCT 继续第 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已暂停导出。提示:再次运行 FEXPORTPRODUCT 继续;运行 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<HoleInfo>();
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<string>(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 snapSurfaces = SnapSurfaceBuilder.BuildSnapSurfaces(panels);
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<EdgeFeatureInfo>();
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.SnapPatterns = mountPatterns;
exportData.SnapSurfaces = snapSurfaces;
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("FEXPORTBUNDLE")]
public void ExportFurnitureBundle()
{
ExportFurniture();
}
[CommandMethod("FEXPORT")]
public void ExportAssetDefault()
{
ExportSingleAssetCore();
}
[CommandMethod("FEXPORTASSET")]
public void ExportSingleAsset()
{
ExportSingleAssetCore();
}
private void ExportSingleAssetCore()
{
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.SnapPatterns = panelAnalysis.SnapPatterns;
exportData.SnapSurfaces = panelAnalysis.SnapSurfaces;
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] 跳过旧装配语义... ✓ (single_asset 使用 snapPatterns/snapSurfaces)\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已清除整套兼容导出暂存会话。请重新运行 FEXPORTPRODUCT。\n");
}
private static List<string> GetEntityHandles(Database db, ObjectId[] ids)
{
var handles = new List<string>();
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<string> handles)
{
var ids = new List<ObjectId>();
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();
}
/// <summary>
/// 查看实体信息命令
/// </summary>
[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();
}
/// <summary>
/// 过滤出 3D Solid 实体
/// </summary>
private ObjectId[] FilterSolids(ObjectId[] ids, Database db)
{
var solids = new List<ObjectId>();
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();
}
/// <summary>
/// 过滤出 3D Solid 和 BlockReference 实体
/// </summary>
private ObjectId[] FilterSolidsAndBlocks(ObjectId[] ids, Database db)
{
var entities = new List<ObjectId>();
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<ObjectId>();
public ObjectId[] HardwareIds { get; set; } = Array.Empty<ObjectId>();
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);
}
/// <summary>
/// 自动模式:一次选择后,尽量自动分组板件与五金件,减少人工操作。
/// </summary>
private AutoSplitResult AutoSplitPanelsAndHardware(ObjectId[] ids, Database db)
{
var panelSet = new HashSet<ObjectId>();
var hardwareSet = new HashSet<ObjectId>();
var allSolidIds = new List<ObjectId>();
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<string> 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;
}
}
/// <summary>
/// 获取板件类型的中文名称
/// </summary>
private static string GetPanelTypeName(string panelType)
{
return panelType switch
{
"side_panel" => "侧板",
"top_panel" => "顶板",
"bottom_panel" => "底板",
"back_panel" => "背板",
"shelf" => "层板",
"vertical_divider" => "竖隔板",
"horizontal_divider" => "横隔板",
"panel" => "板件",
_ => panelType
};
}
/// <summary>
/// 获取五金件类型的中文名称
/// </summary>
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
};
}
}