From eb78978e5036945948006581815ef8b5e2e3c041 Mon Sep 17 00:00:00 2001 From: Tongg Date: Fri, 14 Feb 2025 17:09:43 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=A8=A1=E5=9E=8B=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E3=80=81=E5=9B=BE=E6=A0=87=E8=B4=A8=E9=87=8F?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SparkClient.csproj | 2 +- Views/UserControl/Viewport3D.xaml.cs | 2 +- .../ViewportData/Helper/ObjExporter.cs | 254 ++++++++++-------- logo256.ico | Bin 0 -> 21653 bytes 4 files changed, 138 insertions(+), 120 deletions(-) create mode 100644 logo256.ico 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 0000000000000000000000000000000000000000..1d4e59daf07e8f5f0b4c6ef351727288321ab401 GIT binary patch literal 21653 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