2026-04-03 12:11:24 +08:00

602 lines
25 KiB
C#
Raw Permalink 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.Globalization;
using System.IO;
using System.Text;
using System.Linq;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.BoundaryRepresentation;
namespace FurnitureExporter
{
/// <summary>
/// OBJ 格式导出器 (v2.0)
/// 使用 Brep API 从 AutoCAD Solid3d 提取真实三角网格
/// 保留开孔、倒角等几何细节
/// </summary>
public class ObjExporter
{
private readonly StringBuilder _vertices = new StringBuilder();
private readonly StringBuilder _normals = new StringBuilder();
private readonly StringBuilder _faces = new StringBuilder();
private readonly Dictionary<string, int> _vertexIndexMap = new Dictionary<string, int>();
private readonly Dictionary<string, int> _normalIndexMap = new Dictionary<string, int>();
private int _vertexCount = 0;
private int _normalCount = 0;
private int _solidCount = 0;
private int _faceCount = 0;
private Point3d _originOffset = Point3d.Origin;
// 网格密度控制参数
private double _meshDensityFactor = 45.0; // 默认改为更保守的密度,减少大平面过度细分
private int _maxSubdivisions = 20000; // 默认降低最大细分次数,避免简单板件导出异常膨胀
/// <summary>
/// 设置网格密度因子(默认 100,值越大网格越密,文件越大)
/// </summary>
public void SetMeshDensity(double densityFactor, int maxSubdivisions = 20000)
{
_meshDensityFactor = Math.Max(10, Math.Min(200, densityFactor));
_maxSubdivisions = Math.Max(1000, Math.Min(200000, maxSubdivisions));
}
/// <summary>
/// 设置原点偏移(将模型移动到世界原点)
/// </summary>
public void SetOriginOffset(Point3d offset)
{
_originOffset = offset;
}
private static string BuildVertexKey(Point3d point)
{
return string.Format(
CultureInfo.InvariantCulture,
"{0:F6}|{1:F6}|{2:F6}",
point.X,
point.Y,
point.Z
);
}
private static string BuildNormalKey(Vector3d normal)
{
return string.Format(
CultureInfo.InvariantCulture,
"{0:F6}|{1:F6}|{2:F6}",
normal.X,
normal.Y,
normal.Z
);
}
private int GetOrCreateVertexIndex(Point3d point)
{
var key = BuildVertexKey(point);
if (_vertexIndexMap.TryGetValue(key, out var existingIndex))
{
return existingIndex;
}
_vertices.AppendLine(string.Format(CultureInfo.InvariantCulture,
"v {0:F6} {1:F6} {2:F6}", point.X, point.Y, point.Z));
_vertexCount++;
_vertexIndexMap[key] = _vertexCount;
return _vertexCount;
}
private int GetOrCreateNormalIndex(Vector3d normal)
{
var key = BuildNormalKey(normal);
if (_normalIndexMap.TryGetValue(key, out var existingIndex))
{
return existingIndex;
}
_normals.AppendLine(string.Format(CultureInfo.InvariantCulture,
"vn {0:F6} {1:F6} {2:F6}", normal.X, normal.Y, normal.Z));
_normalCount++;
_normalIndexMap[key] = _normalCount;
return _normalCount;
}
private double CalculateMaxNodeSpacing(Extents3d extents)
{
var size = extents.MinPoint.GetVectorTo(extents.MaxPoint);
var longestEdge = Math.Max(size.X, Math.Max(size.Y, size.Z));
var spacingByDensity = longestEdge / _meshDensityFactor;
// 约束取值:
// - 过小会导致大平面和倒角被极度细分
// - 过大又会明显破坏孔和边缘观感
return Math.Max(4.0, Math.Min(18.0, spacingByDensity));
}
/// <summary>
/// 添加任意支持的实体到 OBJ 导出器
/// </summary>
public void AddEntity(Entity entity, string? name = null)
{
if (entity == null) return;
if (entity is Solid3d solid)
{
AddSolid(solid, name);
return;
}
if (entity is BlockReference blockReference)
{
AddBlockReference(blockReference, name);
}
}
/// <summary>
/// 使用 Brep API 添加一个 Solid3d 实体(提取真实几何)
/// </summary>
public void AddSolid(Solid3d solid, string? name = null)
{
if (solid == null) return;
try
{
_solidCount++;
var solidName = name ?? $"panel_{_solidCount - 1:D3}";
var extents = solid.GeometricExtents;
// 对象名必须与该对象的面一起输出;否则 obj2gltf 会把全部面归到最后一个对象
_faces.AppendLine($"o {solidName}");
// 使用 Brep API 提取真实几何
using (var brep = new Brep(solid))
{
using (var meshControl = new Mesh2dControl())
{
// 设置网格控制参数
// MaxNodeSpacing 控制网格点之间的最大距离
meshControl.MaxNodeSpacing = CalculateMaxNodeSpacing(extents);
meshControl.MaxSubdivisions = (uint)_maxSubdivisions;
using (var meshFilter = new Mesh2dFilter())
{
// 将控制参数应用到 Brep
meshFilter.Insert(brep, meshControl);
using (var mesh = new Mesh2d(meshFilter))
{
// 遍历网格元素(三角形或四边形)
foreach (Element2d element in mesh.Element2ds)
{
var points = new List<Point3d>();
// 提取元素的顶点
foreach (Node node in element.Nodes)
{
// 应用原点偏移
var point = new Point3d(
node.Point.X + _originOffset.X,
node.Point.Y + _originOffset.Y,
node.Point.Z + _originOffset.Z
);
points.Add(point);
// 必须显式 Dispose 节点,否则会崩溃
node.Dispose();
}
// 必须显式 Dispose 元素,否则会崩溃
element.Dispose();
if (points.Count < 3) continue;
// 计算面法线
Vector3d normal = CalculateFaceNormal(points);
var normalIndex = GetOrCreateNormalIndex(normal);
// 写入顶点(复用相同坐标,避免 OBJ 文本重复膨胀)
var vertexIndices = new List<int>();
foreach (var pt in points)
{
vertexIndices.Add(GetOrCreateVertexIndex(pt));
}
// 写入面
if (points.Count == 3)
{
// 三角形
_faces.AppendLine($"f {vertexIndices[0]}//{normalIndex} {vertexIndices[1]}//{normalIndex} {vertexIndices[2]}//{normalIndex}");
_faceCount++;
}
else if (points.Count == 4)
{
// 四边形 - 拆分为两个三角形
_faces.AppendLine($"f {vertexIndices[0]}//{normalIndex} {vertexIndices[1]}//{normalIndex} {vertexIndices[2]}//{normalIndex}");
_faces.AppendLine($"f {vertexIndices[0]}//{normalIndex} {vertexIndices[2]}//{normalIndex} {vertexIndices[3]}//{normalIndex}");
_faceCount += 2;
}
else
{
// 多边形 - 扇形三角化
for (int i = 1; i < points.Count - 1; i++)
{
_faces.AppendLine($"f {vertexIndices[0]}//{normalIndex} {vertexIndices[i]}//{normalIndex} {vertexIndices[i + 1]}//{normalIndex}");
_faceCount++;
}
}
}
}
}
}
}
}
catch (Autodesk.AutoCAD.Runtime.Exception ex)
{
// Brep API 失败时回退到边界框方法
System.Diagnostics.Debug.WriteLine($"Brep 导出失败,回退到边界框: {ex.Message}");
AddSolidBoundingBox(solid, name);
}
catch (System.Exception ex)
{
System.Diagnostics.Debug.WriteLine($"导出 Solid3d 失败: {ex.Message}");
// 尝试回退到边界框
try
{
AddSolidBoundingBox(solid, name);
}
catch
{
// 完全失败,跳过此实体
}
}
}
/// <summary>
/// 尝试展开图块引用并导出内部几何。
/// 如果图块内部没有可导出的 3D 实体,则退化为包围盒模型,至少保证资产中心有模型可用。
/// </summary>
private void AddBlockReference(BlockReference blockReference, string? name = null)
{
if (blockReference == null) return;
var exploded = new DBObjectCollection();
try
{
blockReference.Explode(exploded);
var baseName = name ?? $"block_{_solidCount:D3}";
var childIndex = 0;
var exportedAny = false;
foreach (DBObject dbObject in exploded)
{
if (dbObject is not Entity entity)
{
continue;
}
if (entity is not Solid3d && entity is not BlockReference)
{
continue;
}
AddEntity(entity, $"{baseName}_{childIndex:D3}");
childIndex++;
exportedAny = true;
}
if (!exportedAny)
{
AddBlockReferenceBoundingBox(blockReference, baseName);
}
}
catch (System.Exception ex)
{
System.Diagnostics.Debug.WriteLine($"导出 BlockReference 失败,回退到边界框: {ex.Message}");
AddBlockReferenceBoundingBox(blockReference, name);
}
finally
{
foreach (DBObject dbObject in exploded)
{
dbObject.Dispose();
}
}
}
/// <summary>
/// 图块引用的包围盒导出(回退方案)
/// </summary>
private void AddBlockReferenceBoundingBox(BlockReference blockReference, string? name = null)
{
if (blockReference == null) return;
_solidCount++;
var solidName = name ?? $"block_{_solidCount - 1:D3}";
var extents = blockReference.GeometricExtents;
var min = extents.MinPoint;
var max = extents.MaxPoint;
var minOffset = new Point3d(min.X + _originOffset.X, min.Y + _originOffset.Y, min.Z + _originOffset.Z);
var maxOffset = new Point3d(max.X + _originOffset.X, max.Y + _originOffset.Y, max.Z + _originOffset.Z);
var vertices = new Point3d[]
{
new Point3d(minOffset.X, minOffset.Y, minOffset.Z),
new Point3d(maxOffset.X, minOffset.Y, minOffset.Z),
new Point3d(maxOffset.X, maxOffset.Y, minOffset.Z),
new Point3d(minOffset.X, maxOffset.Y, minOffset.Z),
new Point3d(minOffset.X, minOffset.Y, maxOffset.Z),
new Point3d(maxOffset.X, minOffset.Y, maxOffset.Z),
new Point3d(maxOffset.X, maxOffset.Y, maxOffset.Z),
new Point3d(minOffset.X, maxOffset.Y, maxOffset.Z),
};
_faces.AppendLine($"o {solidName}");
var vertexIndices = vertices.Select(GetOrCreateVertexIndex).ToArray();
var normalIndices = new int[]
{
GetOrCreateNormalIndex(new Vector3d(0, 0, -1)),
GetOrCreateNormalIndex(new Vector3d(0, 0, 1)),
GetOrCreateNormalIndex(new Vector3d(0, -1, 0)),
GetOrCreateNormalIndex(new Vector3d(0, 1, 0)),
GetOrCreateNormalIndex(new Vector3d(-1, 0, 0)),
GetOrCreateNormalIndex(new Vector3d(1, 0, 0)),
};
_faces.AppendLine($"f {vertexIndices[0]}//{normalIndices[0]} {vertexIndices[2]}//{normalIndices[0]} {vertexIndices[1]}//{normalIndices[0]}");
_faces.AppendLine($"f {vertexIndices[0]}//{normalIndices[0]} {vertexIndices[3]}//{normalIndices[0]} {vertexIndices[2]}//{normalIndices[0]}");
_faces.AppendLine($"f {vertexIndices[4]}//{normalIndices[1]} {vertexIndices[5]}//{normalIndices[1]} {vertexIndices[6]}//{normalIndices[1]}");
_faces.AppendLine($"f {vertexIndices[4]}//{normalIndices[1]} {vertexIndices[6]}//{normalIndices[1]} {vertexIndices[7]}//{normalIndices[1]}");
_faces.AppendLine($"f {vertexIndices[0]}//{normalIndices[2]} {vertexIndices[1]}//{normalIndices[2]} {vertexIndices[5]}//{normalIndices[2]}");
_faces.AppendLine($"f {vertexIndices[0]}//{normalIndices[2]} {vertexIndices[5]}//{normalIndices[2]} {vertexIndices[4]}//{normalIndices[2]}");
_faces.AppendLine($"f {vertexIndices[2]}//{normalIndices[3]} {vertexIndices[3]}//{normalIndices[3]} {vertexIndices[7]}//{normalIndices[3]}");
_faces.AppendLine($"f {vertexIndices[2]}//{normalIndices[3]} {vertexIndices[7]}//{normalIndices[3]} {vertexIndices[6]}//{normalIndices[3]}");
_faces.AppendLine($"f {vertexIndices[0]}//{normalIndices[4]} {vertexIndices[4]}//{normalIndices[4]} {vertexIndices[7]}//{normalIndices[4]}");
_faces.AppendLine($"f {vertexIndices[0]}//{normalIndices[4]} {vertexIndices[7]}//{normalIndices[4]} {vertexIndices[3]}//{normalIndices[4]}");
_faces.AppendLine($"f {vertexIndices[1]}//{normalIndices[5]} {vertexIndices[2]}//{normalIndices[5]} {vertexIndices[6]}//{normalIndices[5]}");
_faces.AppendLine($"f {vertexIndices[1]}//{normalIndices[5]} {vertexIndices[6]}//{normalIndices[5]} {vertexIndices[5]}//{normalIndices[5]}");
_faceCount += 12;
}
/// <summary>
/// 计算面法线(使用 Newell 方法,适用于任意多边形)
/// </summary>
private Vector3d CalculateFaceNormal(List<Point3d> points)
{
double nx = 0, ny = 0, nz = 0;
int n = points.Count;
for (int i = 0; i < n; i++)
{
var current = points[i];
var next = points[(i + 1) % n];
nx += (current.Y - next.Y) * (current.Z + next.Z);
ny += (current.Z - next.Z) * (current.X + next.X);
nz += (current.X - next.X) * (current.Y + next.Y);
}
var normal = new Vector3d(nx, ny, nz);
// 归一化
double length = normal.Length;
if (length > 1e-10)
{
normal = normal.DivideBy(length);
}
else
{
// 如果法线接近零,使用默认值
normal = new Vector3d(0, 0, 1);
}
return normal;
}
/// <summary>
/// 使用边界框方法添加实体(作为回退方案)
/// </summary>
private void AddSolidBoundingBox(Solid3d solid, string? name = null)
{
if (solid == null) return;
_solidCount++;
var solidName = name ?? $"panel_{_solidCount - 1:D3}";
// 获取实体的边界框
var extents = solid.GeometricExtents;
var min = extents.MinPoint;
var max = extents.MaxPoint;
// 应用原点偏移
var minOffset = new Point3d(min.X + _originOffset.X, min.Y + _originOffset.Y, min.Z + _originOffset.Z);
var maxOffset = new Point3d(max.X + _originOffset.X, max.Y + _originOffset.Y, max.Z + _originOffset.Z);
// 生成长方体的 8 个顶点
var vertices = new Point3d[]
{
new Point3d(minOffset.X, minOffset.Y, minOffset.Z), // 0: 前左下
new Point3d(maxOffset.X, minOffset.Y, minOffset.Z), // 1: 前右下
new Point3d(maxOffset.X, maxOffset.Y, minOffset.Z), // 2: 后右下
new Point3d(minOffset.X, maxOffset.Y, minOffset.Z), // 3: 后左下
new Point3d(minOffset.X, minOffset.Y, maxOffset.Z), // 4: 前左上
new Point3d(maxOffset.X, minOffset.Y, maxOffset.Z), // 5: 前右上
new Point3d(maxOffset.X, maxOffset.Y, maxOffset.Z), // 6: 后右上
new Point3d(minOffset.X, maxOffset.Y, maxOffset.Z), // 7: 后左上
};
// 对象名必须与该对象的面一起输出;否则 obj2gltf 会把全部面归到最后一个对象
_faces.AppendLine($"o {solidName}");
var vertexIndices = vertices.Select(GetOrCreateVertexIndex).ToArray();
var normalIndices = new int[]
{
GetOrCreateNormalIndex(new Vector3d(0, 0, -1)),
GetOrCreateNormalIndex(new Vector3d(0, 0, 1)),
GetOrCreateNormalIndex(new Vector3d(0, -1, 0)),
GetOrCreateNormalIndex(new Vector3d(0, 1, 0)),
GetOrCreateNormalIndex(new Vector3d(-1, 0, 0)),
GetOrCreateNormalIndex(new Vector3d(1, 0, 0)),
};
// 底面 (Z min)
_faces.AppendLine($"f {vertexIndices[0]}//{normalIndices[0]} {vertexIndices[2]}//{normalIndices[0]} {vertexIndices[1]}//{normalIndices[0]}");
_faces.AppendLine($"f {vertexIndices[0]}//{normalIndices[0]} {vertexIndices[3]}//{normalIndices[0]} {vertexIndices[2]}//{normalIndices[0]}");
// 顶面 (Z max)
_faces.AppendLine($"f {vertexIndices[4]}//{normalIndices[1]} {vertexIndices[5]}//{normalIndices[1]} {vertexIndices[6]}//{normalIndices[1]}");
_faces.AppendLine($"f {vertexIndices[4]}//{normalIndices[1]} {vertexIndices[6]}//{normalIndices[1]} {vertexIndices[7]}//{normalIndices[1]}");
// 前面 (Y min)
_faces.AppendLine($"f {vertexIndices[0]}//{normalIndices[2]} {vertexIndices[1]}//{normalIndices[2]} {vertexIndices[5]}//{normalIndices[2]}");
_faces.AppendLine($"f {vertexIndices[0]}//{normalIndices[2]} {vertexIndices[5]}//{normalIndices[2]} {vertexIndices[4]}//{normalIndices[2]}");
// 后面 (Y max)
_faces.AppendLine($"f {vertexIndices[2]}//{normalIndices[3]} {vertexIndices[3]}//{normalIndices[3]} {vertexIndices[7]}//{normalIndices[3]}");
_faces.AppendLine($"f {vertexIndices[2]}//{normalIndices[3]} {vertexIndices[7]}//{normalIndices[3]} {vertexIndices[6]}//{normalIndices[3]}");
// 左面 (X min)
_faces.AppendLine($"f {vertexIndices[0]}//{normalIndices[4]} {vertexIndices[4]}//{normalIndices[4]} {vertexIndices[7]}//{normalIndices[4]}");
_faces.AppendLine($"f {vertexIndices[0]}//{normalIndices[4]} {vertexIndices[7]}//{normalIndices[4]} {vertexIndices[3]}//{normalIndices[4]}");
// 右面 (X max)
_faces.AppendLine($"f {vertexIndices[1]}//{normalIndices[5]} {vertexIndices[2]}//{normalIndices[5]} {vertexIndices[6]}//{normalIndices[5]}");
_faces.AppendLine($"f {vertexIndices[1]}//{normalIndices[5]} {vertexIndices[6]}//{normalIndices[5]} {vertexIndices[5]}//{normalIndices[5]}");
_faceCount += 12;
}
/// <summary>
/// 保存为 OBJ 文件
/// </summary>
public void Save(string outputPath)
{
var content = new StringBuilder();
// 文件头
content.AppendLine("# Furniture Exporter OBJ File (v2.0 - True Geometry)");
content.AppendLine($"# Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
content.AppendLine($"# Solids: {_solidCount}");
content.AppendLine($"# Vertices: {_vertexCount}");
content.AppendLine($"# Normals: {_normalCount}");
content.AppendLine($"# Faces: {_faceCount}");
content.AppendLine();
// 顶点
content.Append(_vertices);
content.AppendLine();
// 法线
content.Append(_normals);
content.AppendLine();
// 面(包含对象分组标记)
content.Append(_faces);
File.WriteAllText(outputPath, content.ToString(), Encoding.UTF8);
}
/// <summary>
/// 获取统计信息
/// </summary>
public (int Solids, int Vertices, int Normals, int Faces) GetStats()
{
return (_solidCount, _vertexCount, _normalCount, _faceCount);
}
/// <summary>
/// 导出多个实体到 OBJ 文件
/// </summary>
/// <param name="solidIds">实体 ID 数组</param>
/// <param name="outputPath">输出文件路径</param>
/// <param name="originOffset">原点偏移(可选,用于将模型移动到世界原点)</param>
/// <param name="meshDensity">网格密度因子(可选,默认 45)</param>
/// <param name="maxSubdivisions">最大细分次数(可选,默认 20000)</param>
public static void Export(
ObjectId[] solidIds,
string outputPath,
Point3d? originOffset = null,
double meshDensity = 45,
int maxSubdivisions = 20000)
{
var exporter = new ObjExporter();
if (originOffset.HasValue)
{
exporter.SetOriginOffset(originOffset.Value);
}
exporter.SetMeshDensity(meshDensity, maxSubdivisions);
var db = solidIds[0].Database;
using (var tr = db.TransactionManager.StartTransaction())
{
int index = 0;
foreach (var id in solidIds)
{
var solid = tr.GetObject(id, OpenMode.ForRead) as Solid3d;
if (solid != null)
{
exporter.AddSolid(solid, $"panel_{index:D3}");
index++;
}
}
tr.Commit();
}
exporter.Save(outputPath);
}
/// <summary>
/// 导出多个实体(3D Solid / BlockReference)到 OBJ 文件
/// </summary>
public static void ExportEntities(
ObjectId[] entityIds,
string outputPath,
Database db,
Point3d? originOffset = null,
double meshDensity = 45,
int maxSubdivisions = 20000)
{
var exporter = new ObjExporter();
if (originOffset.HasValue)
{
exporter.SetOriginOffset(originOffset.Value);
}
exporter.SetMeshDensity(meshDensity, maxSubdivisions);
using (var tr = db.TransactionManager.StartTransaction())
{
int index = 0;
foreach (var id in entityIds)
{
var entity = tr.GetObject(id, OpenMode.ForRead) as Entity;
if (entity == null)
{
continue;
}
exporter.AddEntity(entity, $"asset_{index:D3}");
index++;
}
tr.Commit();
}
exporter.Save(outputPath);
}
}
}