diff --git a/SparkClient.csproj b/SparkClient.csproj index 8d59447..ee99a1c 100644 --- a/SparkClient.csproj +++ b/SparkClient.csproj @@ -6,7 +6,7 @@ enable enable true - logo.ico + logo256.ico SparkClient - 星辉 SparkClient Team SparkClient钻石检测工具 diff --git a/Views/UserControl/Viewport3D.xaml.cs b/Views/UserControl/Viewport3D.xaml.cs index c1c3c6b..22b2d69 100644 --- a/Views/UserControl/Viewport3D.xaml.cs +++ b/Views/UserControl/Viewport3D.xaml.cs @@ -220,7 +220,7 @@ public partial class Viewport3D break; case "BtnShow3DView": ObjExporter.ExportToObj2(ViewportManager.ViewportTriangle, @"D:\id03.obj"); - UnityHelper.GenerateRender(ViewportManager.ViewportTriangle.First().TriangleCode, "123"); + // UnityHelper.GenerateRender(ViewportManager.ViewportTriangle.First().TriangleCode, "123"); break; } diff --git a/Views/UserControl/ViewportData/Helper/ObjExporter.cs b/Views/UserControl/ViewportData/Helper/ObjExporter.cs index 3064d1d..64391e0 100644 --- a/Views/UserControl/ViewportData/Helper/ObjExporter.cs +++ b/Views/UserControl/ViewportData/Helper/ObjExporter.cs @@ -1,5 +1,6 @@ using System.IO; using System.Text; +using HelixToolkit.SharpDX.Core; using SharpDX; using SparkClient.Views.UserControl.ViewportData.Entity; using SparkClient.Views.UserControl.ViewportData.Enum; @@ -8,154 +9,171 @@ namespace SparkClient.Views.UserControl.ViewportData.Helper; public class ObjExporter { - public static void ExportToObj2(List entities, string outputPath) +{ + // 分组:按面代码 PlaneCode 分组 + var faceGroups = entities + .GroupBy(e => e.PlaneCode) + .ToDictionary(g => g.Key, g => g.ToList()); + + // 存储每个面的顶点和法线 + var faceData = new Dictionary(); + + // Step 1: 处理每个面,生成顶点列表并计算法线 + foreach (var group in faceGroups) { - //分组 - Dictionary> feactList = entities - .Where(entity => entity.PlaneType != PlaneType.Girdle) - .GroupBy(entity => entity.PlaneCode) - .ToDictionary(group => group.Key, group => group.ToList()); - //腰 单组 - // List waistList = entities - // .Where(entity => entity.PlaneType == PlaneType.Girdle) - // .ToList(); - Dictionary> gridleList = entities - .Where(entity => entity.PlaneType == PlaneType.Girdle) - .GroupBy(entity => entity.PlaneCode) - .ToDictionary(group => group.Key, group => group.ToList()); - - //同一个面只保留外边框(除了腰) - Dictionary> resultPoints = new Dictionary>(); - - foreach (var dic in feactList) - { - List tempPoints = new List(); - foreach (var entity in dic.Value) - { - tempPoints.Add(entity.Point1); - tempPoints.Add(entity.Point2); - tempPoints.Add(entity.Point3); - } - resultPoints.Add(dic.Key, ViewportHelperPro.VectorClockwiseSort(new HashSet(tempPoints).ToList())); - } - - foreach (var dic in gridleList) - { - List tempPoints = new List(); - foreach (var entity in dic.Value) - { - tempPoints.Add(entity.Point1); - tempPoints.Add(entity.Point2); - tempPoints.Add(entity.Point3); - } - resultPoints.Add(dic.Key, ViewportHelperPro.VectorClockwiseSort(new HashSet(tempPoints).ToList())); - } - // List selFaceVector = new List(); - // if (waistList.Count > 0) - // { - // foreach (var entity in waistList) - // { - // selFaceVector.Add(entity.Point1); - // selFaceVector.Add(entity.Point2); - // selFaceVector.Add(entity.Point3); - // } - // } - // resultPoints.Add("yao", selFaceVector); - - StringBuilder sb = new StringBuilder(); - - // 顶点列表 - List uniqueVertices = new List(); - Dictionary vertexIndexMap = new Dictionary(); - - // 1. 对每个面生成顶点和面 - foreach (var face in resultPoints) + // 合并所有顶点并去重 + var vertices = group.Value + .SelectMany(e => new[] { e.Point1, e.Point2, e.Point3 }) + .Distinct() + .ToList(); + + // 按凸包算法排序顶点(避免交叉) + var sortedVertices = ConvexHullSort(vertices); + + // 计算面的法线(基于排序后的顶点) + var normal = CalculateFaceNormal(sortedVertices, ViewportManager.CenterVector); + + // 存储面数据 + faceData[group.Key] = new FaceData { - // 对每个面,获取它的顶点列表 - List faceVertices = face.Value; + Vertices = sortedVertices, + Normal = normal + }; + } - // 去重顶点 - foreach (var vertex in faceVertices) + // Step 2: 写入 OBJ 文件 + using (var writer = new StreamWriter(outputPath)) + { + // 写入顶点 + var vertexIndexMap = new Dictionary(); + int index = 1; + foreach (var face in faceData.Values) + { + foreach (var vertex in face.Vertices) { if (!vertexIndexMap.ContainsKey(vertex)) { - vertexIndexMap[vertex] = uniqueVertices.Count; - uniqueVertices.Add(vertex); + vertexIndexMap[vertex] = index++; + writer.WriteLine($"v {vertex.X} {vertex.Y} {vertex.Z}"); } } } - // 2. 写入顶点数据 - foreach (var vertex in uniqueVertices) - { - sb.AppendLine($"v {vertex.X} {vertex.Y} {vertex.Z}"); - } - - // 3. 写入每个面(f行),包括正面和反面 - foreach (var face in resultPoints) + // 写入法线(每个面一个法线) + var normalIndexMap = new Dictionary(); + index = 1; + foreach (var face in faceData.Values) { - sb.AppendLine($"# Face: {face.Key}"); - - // 获取面上的所有顶点并排序 - List faceVertices = face.Value; - if (!face.Key.StartsWith("11_")) + if (!normalIndexMap.ContainsKey(face.Normal)) { - Vector3 center = GetCenterOfVertices(faceVertices); - faceVertices.Sort((v1, v2) => GetAngle(v1, center).CompareTo(GetAngle(v2, center))); + normalIndexMap[face.Normal] = index++; + // writer.WriteLine($"vn {face.Normal.X} {face.Normal.Y} {face.Normal.Z}"); } + } - + // 写入面 + foreach (var faceEntry in faceData) + { + var face = faceEntry.Value; + var normalIndex = normalIndexMap[face.Normal]; - // 正面:按顺时针顺序输出 - sb.Append("f"); - foreach (var vertex in faceVertices) + // 正面(顺时针) + writer.Write("f "); + foreach (var vertex in face.Vertices) { - sb.Append($" {vertexIndexMap[vertex] + 1}"); + writer.Write($"{vertexIndexMap[vertex]}//{normalIndex} "); } - sb.AppendLine(); + writer.WriteLine(); - // 反面:按逆时针顺序输出(反转顶点顺序) - sb.Append("f"); - for (int i = faceVertices.Count - 1; i >= 0; i--) + // 反面(逆时针) + writer.Write("f "); + foreach (var vertex in face.Vertices.AsEnumerable().Reverse()) { - sb.Append($" {vertexIndexMap[faceVertices[i]] + 1}"); + writer.Write($"{vertexIndexMap[vertex]}//{normalIndex} "); } - sb.AppendLine(); + writer.WriteLine(); } - - // 4. 写入文件 - File.WriteAllText(outputPath, sb.ToString()); - } - - // 计算一组顶点的中心点(用于排序) - private static Vector3 GetCenterOfVertices(List vertices) +} + + /// + /// 凸包排序(Andrew's Monotone Chain 算法) + /// + private static List ConvexHullSort(List points) { - float centerX = 0, centerY = 0, centerZ = 0; - foreach (var vertex in vertices) + if (points.Count <= 3) return points; + + // 按 X 坐标排序,若相同则按 Y 排序 + var sorted = points.OrderBy(p => p.X).ThenBy(p => p.Y).ToList(); + + // 构建下凸包和上凸包 + var lower = new List(); + foreach (var p in sorted) { - centerX += vertex.X; - centerY += vertex.Y; - centerZ += vertex.Z; + while (lower.Count >= 2 && Cross(lower[lower.Count - 2], lower[lower.Count - 1], p) <= 0) + lower.RemoveAt(lower.Count - 1); + lower.Add(p); } - return new Vector3(centerX / vertices.Count, centerY / vertices.Count, centerZ / vertices.Count); - } - - private static float GetAngle(Vector3 vertex, Vector3 center) - { - // 计算顶点与中心的方向向量 - Vector3 direction = vertex - center; - // 在XY平面上计算角度 - float angle = (float)Math.Atan2(direction.Y, direction.X); // 返回的是弧度,[-π, π] + var upper = new List(); + foreach (var p in sorted.AsEnumerable().Reverse()) + { + while (upper.Count >= 2 && Cross(upper[upper.Count - 2], upper[upper.Count - 1], p) <= 0) + upper.RemoveAt(upper.Count - 1); + upper.Add(p); + } - // 如果你想要角度范围 [0, 2π],可以做如下处理 - if (angle < 0) + // 合并凸包(移除重复点) + lower.RemoveAt(lower.Count - 1); + upper.RemoveAt(upper.Count - 1); + return lower.Concat(upper).ToList(); + } + + /// + /// 计算叉积 (p2-p0) × (p1-p0) + /// + private static float Cross(Vector3 p0, Vector3 p1, Vector3 p2) + { + return (p1.X - p0.X) * (p2.Z - p0.Z) - (p1.Z - p0.Z) * (p2.X - p0.X); + } + /// + /// 计算面的法线(基于顶点顺序) + /// + private static Vector3 CalculateFaceNormal(List vertices, Vector3 modelCenter) + { + // 使用前三个顶点计算初始法线 + var p0 = vertices[0]; + var p1 = vertices[1]; + var p2 = vertices[2]; + var edge1 = p1 - p0; + var edge2 = p2 - p0; + var normal = Vector3.Cross(edge1, edge2).Normalized(); + + // 确保法线朝外(面中心到模型中心的向量与法线方向一致) + var faceCenter = GetCenter(vertices); + var toModelCenter = modelCenter - faceCenter; + if (Vector3.Dot(normal, toModelCenter) > 0) { - angle += MathF.PI * 2; + normal = -normal; } - return angle; + return normal; } + + /// + /// 计算顶点列表的中心点 + /// + private static Vector3 GetCenter(List vertices) + { + var center = Vector3.Zero; + foreach (var v in vertices) center += v; + return center / vertices.Count; + } +} + +class FaceData +{ + public List Vertices { get; set; } + public Vector3 Normal { get; set; } } \ No newline at end of file diff --git a/logo256.ico b/logo256.ico new file mode 100644 index 0000000..1d4e59d Binary files /dev/null and b/logo256.ico differ