feat: 添加3D控件

master
Tongg 7 months ago
parent c4d442ebd7
commit 6a7ad27f73
  1. 1
      App.xaml
  2. 2
      MainWindow.xaml.cs
  3. 129
      Model/Entity/Viewport3DTriangleEntity.cs
  4. 825
      Model/Helper/Viewport3DHelper.cs
  5. 9
      SparkClient.sln.DotSettings.user
  6. 10
      ViewModel/Grading/GradingResultVM.cs
  7. 6
      Views/Grading/GradingResult.xaml
  8. 2
      Views/Grading/GradingResult.xaml.cs
  9. 76
      Views/UserControl/Viewport3D.xaml
  10. 157
      Views/UserControl/Viewport3D.xaml.cs
  11. 6
      Views/UserControl/ViewportData/Entity/MeasurementsDataEntity.cs
  12. 41
      Views/UserControl/ViewportData/Entity/Viewport3DTriangleEntity.cs
  13. 31
      Views/UserControl/ViewportData/Enum/PlaneType.cs
  14. 78
      Views/UserControl/ViewportData/Helper/CommonHelper.cs
  15. 873
      Views/UserControl/ViewportData/Helper/ViewportHelperPro.cs
  16. 355
      Views/UserControl/ViewportData/Helper/ViewportManager.cs
  17. 143
      Views/UserControl/ViewportData/ViewportData.cs

@ -17,6 +17,7 @@
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/>
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml"/>
<!-- <ResourceDictionary Source="pack://application:,,,/HandyControl;component/themes/skindark.xaml"/> -->
<ResourceDictionary Source="/Language/en_US.xaml"/>
<ResourceDictionary Source="/Language/zh_CN.xaml"/>
</ResourceDictionary.MergedDictionaries>

@ -10,6 +10,7 @@ using log4net.Config;
using SparkClient.Model.Helper;
using SparkClient.ViewModel.BaseWindow;
using SparkClient.Views.BaseWindow;
using Window = System.Windows.Window;
namespace SparkClient;
@ -23,7 +24,6 @@ public partial class MainWindow
public MainWindow()
{
InitializeComponent();
Logger.Info("Application started");
}

@ -1,129 +0,0 @@
using System.Security.Cryptography;
using System.Text;
using SharpDX;
using SparkClient.Model.Enums;
namespace SparkClient.Model.Entity;
/// <summary>
/// 三角形
/// </summary>
public class Viewport3DTriangleEntity
{
/// <summary>
/// 点1
/// </summary>
public Vector3 Point1 { get; set; }
/// <summary>
/// 点2
/// </summary>
public Vector3 Point2 { get; set; }
/// <summary>
/// 点3
/// </summary>
public Vector3 Point3 { get; set; }
/// <summary>
/// 三角形代码[生成]
/// 按顺序:p1.x,p1.y,p1.z;p2.x,p2.y,p2.z;p3.x,p3.y,p3.z 拼接后使用生成大写16位md5
/// </summary>
public String TriangleCode { get; set; }
/// <summary>
/// 面代码
/// 由多个三角形组成的面的代码:entity1、entity2组成了一个正方形,那么他俩的TriangleCode属性值一致
/// </summary>
public String PlaneCode { get; set; }
/// <summary>
/// 面类型
/// 比如这个面时钻石的腰部分,那么type统一,当面类型为err时则该面是一个异常面
/// 可选值:PlaneType
/// </summary>
public PlaneType PlaneType { get; set; }
public static Viewport3DTriangleEntity CreateByString(string str)
{
if (string.IsNullOrWhiteSpace(str))
{
throw new ArgumentException("Input string cannot be null or empty");
}
// 分割为四段
var parts = str.Split('|');
if (parts.Length != 4)
{
throw new ArgumentException("Input string format is incorrect. Expected 4 parts separated by '|'.");
}
// 解析第一段:坐标
var coordinates = parts[0].Split(';');
if (coordinates.Length != 3)
{
throw new ArgumentException("Coordinates part must contain exactly 3 points separated by ';'.");
}
var points = coordinates.Select(ParseVector3).ToArray();
// 解析第二段:TriangleCode [内部标记]
// 解析第三段:PlaneCode
string planeCode = parts[2];
if (string.IsNullOrWhiteSpace(planeCode))
{
throw new ArgumentException("PlaneCode cannot be null or empty");
}
// 解析第四段:PlaneType
if (!int.TryParse(parts[3], out int planeTypeValue) || !Enum.IsDefined(typeof(PlaneType), planeTypeValue))
{
throw new ArgumentException("Invalid PlaneType value");
}
PlaneType planeType = (PlaneType)planeTypeValue;
// 生成 TriangleCode 格式:p1.x,p1.y,p1.z;p2.x,p2.y,p2.z;p3.x,p3.y,p3.z 生成 MD5
string concatenatedPoints = string.Join(";", points.Select(p => $"{p.X},{p.Y},{p.Z}"));
string generatedTriangleCode = GenerateMD5Hash(concatenatedPoints);
return new Viewport3DTriangleEntity
{
Point1 = points[0],
Point2 = points[1],
Point3 = points[2],
TriangleCode = generatedTriangleCode,
PlaneCode = planeCode,
PlaneType = planeType
};
}
// public static Viewport3DTriangleEntity CreateByJsonStr(string str)
// {
//
// }
private static Vector3 ParseVector3(string coordinate)
{
var values = coordinate.Split(',');
if (values.Length != 3)
{
throw new ArgumentException("Each coordinate must contain exactly 3 values separated by ','");
}
if (!float.TryParse(values[0], out float x) ||
!float.TryParse(values[1], out float y) ||
!float.TryParse(values[2], out float z))
{
throw new ArgumentException("Coordinate values must be valid floating-point numbers");
}
return new Vector3(x, y, z);
}
public static string GenerateMD5Hash(string input)
{
using (var md5 = MD5.Create())
{
var inputBytes = Encoding.UTF8.GetBytes(input);
var hashBytes = md5.ComputeHash(inputBytes);
// 转换为大写的16位十六进制字符串
return string.Concat(hashBytes.Take(8).Select(b => b.ToString("X2")));
}
}
}

@ -1,825 +0,0 @@
using System.IO;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using SharpDX.Direct3D11;
using SparkClient.Model.Entity;
namespace SparkClient.Model.Helper;
using HelixToolkit.Wpf.SharpDX;
using SharpDX;
using System.Collections.Generic;
public class Viewport3DHelper
{
// 加载环境贴图
// public static Stream stream =
// File.Open("D:\\WorkSpace\\spark\\HelixToolkitSharpDX-Demo\\morning_racing_circuit_16k.dds", FileMode.Open);
/// <summary>
/// 生成一个多边形钻石
/// </summary>
/// <param name="viewport">控件</param>
/// <param name="sides">边数</param>
public static void GenerateDiamond(Viewport3DX viewport, int sides)
{
// 启用法线生成
var meshBuilder = new MeshBuilder(true, false);
//设置抗锯齿
viewport.RenderHost.MSAA = MSAALevel.Maximum;
double radius = 3.2915;
double smallRadius = radius / 2;
double topHeight = 1.67;
double bottomHeight = 4.43;
double smallOffset = 1.25;
var largeVertices = new List<Vector3>();
var smallVertices = new List<Vector3>();
var edgeLines = new List<Tuple<Vector3, Vector3>>();
// 生成大八边形顶点
for (int i = 0; i < sides; i++)
{
double angle = i * Math.PI / (sides / 2);
largeVertices.Add(new Vector3((float)(radius * Math.Cos(angle)), (float)topHeight,
(float)(radius * Math.Sin(angle))));
}
// 生成小八边形顶点
for (int i = 0; i < sides; i++)
{
double angle = i * Math.PI / (sides / 2);
smallVertices.Add(new Vector3((float)(smallRadius * Math.Cos(angle)), (float)(topHeight + smallOffset),
(float)(smallRadius * Math.Sin(angle))));
}
var bottomPoint = new Vector3(0, (float)(topHeight - bottomHeight), 0);
var topPoint = new Vector3(0, (float)topHeight, 0);
// 生成底部面
for (int i = 0; i < sides; i++)
{
int nextIndex = (i + 1) % sides;
// 计算法线
// Vector3 edge1 = largeVertices[nextIndex] - bottomPoint;
// Vector3 edge2 = largeVertices[i] - bottomPoint;
// Vector3 normal = Vector3.Cross(edge1, edge2);
// normal.Normalize();
List<Vector3> fanPositions2 = new List<Vector3>()
{ bottomPoint, largeVertices[nextIndex], largeVertices[i] };
List<Vector3> fanNormals2 = new List<Vector3>()
{ new Vector3(0, 1, 0), new Vector3(0, 1, 0), new Vector3(0, 1, 0) };
meshBuilder.AddTriangleFan(fanPositions2, fanNormals2);
// meshBuilder.AddTriangle(bottomPoint, largeVertices[nextIndex], largeVertices[i]);
// meshBuilder.AddTriangle(topPoint, largeVertices[i], largeVertices[nextIndex]);
// 添加边框线
edgeLines.Add(new Tuple<Vector3, Vector3>(bottomPoint, largeVertices[nextIndex]));
edgeLines.Add(new Tuple<Vector3, Vector3>(largeVertices[nextIndex], largeVertices[i]));
edgeLines.Add(new Tuple<Vector3, Vector3>(largeVertices[i], bottomPoint));
}
// 生成法线并添加小八边形的面
var smallNormals = new List<Vector3>();
for (int i = 0; i < smallVertices.Count; i++)
{
smallNormals.Add(new Vector3(0, 1, 0)); // 小八边形法线向上
}
meshBuilder.AddTriangleFan(smallVertices, smallNormals);
// 生成大八边形和小八边形的连接面
for (int i = 0; i < sides; i++)
{
int nextIndex = (i + 1) % sides;
// 计算法线
// Vector3 edge1 = largeVertices[nextIndex] - largeVertices[i];
// Vector3 edge2 = smallVertices[nextIndex] - largeVertices[i];
// Vector3 normal1 = Vector3.Cross(edge1, edge2);
// normal1.Normalize();
//
// Vector3 edge3 = smallVertices[nextIndex] - largeVertices[i];
// Vector3 edge4 = smallVertices[i] - largeVertices[i];
// Vector3 normal2 = Vector3.Cross(edge3, edge4);
// normal2.Normalize();
List<Vector3> fanPositions1 = new List<Vector3>()
{ largeVertices[i], largeVertices[nextIndex], smallVertices[nextIndex] };
List<Vector3> fanNormals1 = new List<Vector3>()
{ new Vector3(0, 1, 0), new Vector3(0, 1, 0), new Vector3(0, 1, 0) };
meshBuilder.AddTriangleFan(fanPositions1, fanNormals1);
//meshBuilder.AddTriangle(largeVertices[i], largeVertices[nextIndex], smallVertices[nextIndex]);
List<Vector3> fanPositions2 = new List<Vector3>()
{ largeVertices[i], smallVertices[nextIndex], smallVertices[i] };
List<Vector3> fanNormals2 = new List<Vector3>()
{ new Vector3(0, 1, 0), new Vector3(0, 1, 0), new Vector3(0, 1, 0) };
meshBuilder.AddTriangleFan(fanPositions2, fanNormals2);
//meshBuilder.AddTriangle(largeVertices[i], smallVertices[nextIndex], smallVertices[i]);
// 添加边框线
edgeLines.Add(new Tuple<Vector3, Vector3>(largeVertices[i], largeVertices[nextIndex]));
edgeLines.Add(new Tuple<Vector3, Vector3>(largeVertices[nextIndex], smallVertices[nextIndex]));
edgeLines.Add(new Tuple<Vector3, Vector3>(smallVertices[nextIndex], smallVertices[i]));
edgeLines.Add(new Tuple<Vector3, Vector3>(smallVertices[i], largeVertices[i]));
}
var mesh = meshBuilder.ToMeshGeometry3D();
var material = new PBRMaterial
{
AlbedoColor = new Color4(0.11f, 0.56f, 1f, 0.6f), // 纯白色
MetallicFactor = 0.0,
RoughnessFactor = 0.01,
ReflectanceFactor = 1.0,
ClearCoatStrength = 1.0,
ClearCoatRoughness = 0.0,
// RenderEnvironmentMap = true,
// IrradianceMap = TextureModel.Create(stream), // 环境贴图
};
// 设置材质的采样器状态
material.SurfaceMapSampler = new SamplerStateDescription
{
Filter = Filter.MinMagMipLinear,
AddressU = TextureAddressMode.Wrap,
AddressV = TextureAddressMode.Wrap,
AddressW = TextureAddressMode.Wrap
};
//边框
var lineBuilder = new LineBuilder();
foreach (var line in edgeLines)
{
lineBuilder.AddLine(line.Item1, line.Item2);
}
var edgeLinesModel = new LineGeometryModel3D
{
Geometry = lineBuilder.ToLineGeometry3D(),
Color = System.Windows.Media.Colors.Black,
Thickness = 1.0
};
viewport.Items.Add(edgeLinesModel);
var geometryModel = new MeshGeometryModel3D
{
Geometry = mesh,
Material = material
};
viewport.Items.Add(geometryModel);
viewport.ZoomExtents();
}
public static void GenerateModelByEntity(Viewport3DX viewport, List<Viewport3DTriangleEntity> entities)
{
var meshBuilder = new MeshBuilder(true, false);
viewport.RenderHost.MSAA = MSAALevel.Maximum;
foreach (var entity in entities)
{
List<Vector3> vertices = new List<Vector3>(){entity.Point1, entity.Point2, entity.Point3};
meshBuilder.AddPolygon(vertices);
}
var mesh = meshBuilder.ToMeshGeometry3D();
var material = new PBRMaterial
{
AlbedoColor = new Color4(0.11f, 0.56f, 1f, 0.6f),
MetallicFactor = 0.0,
RoughnessFactor = 0.01,
ReflectanceFactor = 1.0,
ClearCoatStrength = 1.0,
ClearCoatRoughness = 0.0,
};
// 设置材质的采样器状态
material.SurfaceMapSampler = new SamplerStateDescription
{
Filter = Filter.MinMagMipLinear,
AddressU = TextureAddressMode.Wrap,
AddressV = TextureAddressMode.Wrap,
AddressW = TextureAddressMode.Wrap
};
var geometryModel = new MeshGeometryModel3D
{
Geometry = mesh,
Material = material
};
viewport.Items.Add(geometryModel);
}
/// <summary>
/// 传入一个三角形,找到关联面
/// </summary>
/// <param name="viewport">控件</param>
/// <param name="triangleIndex">三角形顶点</param>
/// <param name="selColor">选择主体颜色</param>
/// <param name="linkColor">选择关联颜色(null不渲染)</param>
/// <param name="borderColor">选择边框颜色(null不渲染)</param>
/// <returns></returns>
public static List<GeometryModel3D> HighlightAssociatedFaces(Viewport3DX viewport,
Tuple<Vector3, Vector3, Vector3> triangleIndex, Color4 selColor, Color4? linkColor = null,
Color4? borderColor = null)
{
List<GeometryModel3D> result = new List<GeometryModel3D>();
//选中平面上的点(实现选中高亮、边框线)
List<Vector3> selFaceVector = new List<Vector3>();
selFaceVector.Add(triangleIndex.Item1);
selFaceVector.Add(triangleIndex.Item2);
selFaceVector.Add(triangleIndex.Item3);
foreach (var item in viewport.Items)
{
if (item is MeshGeometryModel3D geometryModel)
{
var geometry = geometryModel.Geometry;
if (geometry != null)
{
var positions = geometry.Positions; // 顶点列表
var indices = geometry.Indices; // 索引列表
// 确保 Indices 是三角形格式
for (int i = 0; i < indices.Count; i += 3)
{
// 获取三角形的三个顶点索引
int index0 = indices[i];
int index1 = indices[i + 1];
int index2 = indices[i + 2];
// 获取三角形的三个顶点坐标
var vertex0 = positions[index0];
var vertex1 = positions[index1];
var vertex2 = positions[index2];
List<Vector3> temp = new List<Vector3>() { vertex0, vertex1, vertex2 };
if (ArePointsOnSamePlane(triangleIndex, temp))
{
//所有点共面,两种情况,三角形自己,三角形相连在同一个平面
if (temp.Count == 3 && temp.Contains(triangleIndex.Item1) &&
temp.Contains(triangleIndex.Item2) && temp.Contains(triangleIndex.Item3))
continue;
foreach (var vector in temp)
{
// if(!selFaceVector.Contains(vector))
selFaceVector.Add(vector);
}
}
}
}
}
}
//当前选中的平面
var meshBuilder = new MeshBuilder(true, false);
List<Vector3> fanNormals2 = new List<Vector3>();
foreach (var vertex in selFaceVector)
fanNormals2.Add(new Vector3(0, 1, 0));
meshBuilder.AddTriangleFan(selFaceVector, fanNormals2);
var mesh = meshBuilder.ToMeshGeometry3D();
var material = new PBRMaterial
{
AlbedoColor = selColor, // 纯白色
MetallicFactor = 0,
RoughnessFactor = 1,
ReflectanceFactor = 0.5,
};
var geometryModel1 = new MeshGeometryModel3D
{
Geometry = mesh,
Material = material
};
//当前选中平面边框线
var edgeLines = new List<Tuple<Vector3, Vector3>>();
var lengths = new List<double>(); // 长度列表
var angles = new List<double>(); // 夹角列表
HashSet<Vector3> uniqueVectors = new HashSet<Vector3>(selFaceVector);
List<Vector3> sortedVectors = uniqueVectors.ToList();
Vector3 center = GetCentroid(sortedVectors);
// 按极角顺时针排序
sortedVectors.Sort((v1, v2) =>
{
double angle1 = Math.Atan2(v1.Z - center.Z, v1.X - center.X); // 计算第一个点的角度
double angle2 = Math.Atan2(v2.Z - center.Z, v2.X - center.X); // 计算第二个点的角度
return angle1.CompareTo(angle2); // 按角度从小到大排序
});
for (int i = 0; i < sortedVectors.Count - 1; i++)
{
var nowItem = sortedVectors[i];
var nextItem = sortedVectors[i + 1];
edgeLines.Add(new Tuple<Vector3, Vector3>(nowItem, nextItem));
// 计算当前线段的长度
lengths.Add((nextItem - nowItem).Length());
}
edgeLines.Add(new Tuple<Vector3, Vector3>(sortedVectors.Last(), sortedVectors.First()));
lengths.Add((sortedVectors.Last() - sortedVectors.First()).Length());
//计算夹角
for (int i = 0; i < sortedVectors.Count; i++)
{
int prevIndex = (i - 1 + sortedVectors.Count) % sortedVectors.Count;
int nextIndex = (i + 1) % sortedVectors.Count;
// 获取三点
var p1 = sortedVectors[prevIndex];
var p2 = sortedVectors[i];
var p3 = sortedVectors[nextIndex];
// 计算夹角
var v1 = p2 - p1;
var v2 = p3 - p2;
double angle = AngleBetween(v1, v2);
angles.Add(angle); // 夹角,单位是度
}
if (sortedVectors.Count == lengths.Count && sortedVectors.Count == angles.Count)
{
for (int i = 0; i < sortedVectors.Count; i++)
{
var point = sortedVectors[i];
var pointNext = i + 1 >= sortedVectors.Count ? sortedVectors.First() : sortedVectors[i + 1];
var lengthText = $"L: {lengths[i]:F2}";
var angleText = $"Angle: {angles[i]:F2}°";
var lengthTextPoint = (pointNext + point) / 2;
// 在合适位置显示文本
var text3DL = DisplayText3D(lengthText,
new Vector3(lengthTextPoint.X, lengthTextPoint.Y + 0.5f, lengthTextPoint.Z));
var text3DA = DisplayText3D(angleText, new Vector3(point.X, point.Y + 0.5f, point.Z + 0.5f));
viewport.Items.Add(text3DL);
viewport.Items.Add(text3DA);
result.Add(text3DL);
result.Add(text3DA);
}
}
else
{
// 如果列表长度不一致,给出提示信息
Console.WriteLine("The lengths or angles list size does not match the sorted vectors list.");
}
var lineBuilder = new LineBuilder();
foreach (var line in edgeLines)
{
lineBuilder.AddLine(line.Item1, line.Item2);
}
var edgeLinesModel = new LineGeometryModel3D
{
Geometry = lineBuilder.ToLineGeometry3D(),
Color = System.Windows.Media.Colors.Red,
Thickness = 1.0
};
viewport.Items.Add(edgeLinesModel);
viewport.Items.Add(geometryModel1);
result.Add(geometryModel1);
result.Add(edgeLinesModel);
return result;
}
/// <summary>
/// 遍历视图中的所有三角形
/// </summary>
/// <param name="viewport"></param>
/// <returns></returns>
public static List<Tuple<Vector3, Vector3, Vector3>> ForeachViewPortTriangle(Viewport3DX viewport)
{
List<Tuple<Vector3, Vector3, Vector3>> result = new List<Tuple<Vector3, Vector3, Vector3>>();
foreach (var item in viewport.Items)
{
if (item is MeshGeometryModel3D geometryModel)
{
var geometry = geometryModel.Geometry;
if (geometry != null)
{
var positions = geometry.Positions; // 顶点列表
var indices = geometry.Indices; // 索引列表
// 确保 Indices 是三角形格式
for (int i = 0; i < indices.Count; i += 3)
{
// 获取三角形的三个顶点索引
int index0 = indices[i];
int index1 = indices[i + 1];
int index2 = indices[i + 2];
// 获取三角形的三个顶点坐标
var vertex0 = positions[index0];
var vertex1 = positions[index1];
var vertex2 = positions[index2];
result.Add(new Tuple<Vector3, Vector3, Vector3>(vertex0, vertex1, vertex2));
}
}
}
}
return result;
}
private static BillboardTextModel3D DisplayText3D(string text, Vector3 position)
{
var billboardTextModel = new BillboardTextModel3D();
var billboardText = new BillboardText3D();
billboardText.TextInfo.Add(new TextInfo(text, position)
{
Foreground = Color.Green,
Scale = 0.5f,
});
billboardTextModel.Geometry = billboardText;
return billboardTextModel;
}
public static float AngleBetween(Vector3 v1, Vector3 v2)
{
// 计算两个向量的点积
float dotProduct = Vector3.Dot(v1, v2);
// 计算每个向量的模长
float magnitudeV1 = v1.Length();
float magnitudeV2 = v2.Length();
// 防止除以零错误
if (magnitudeV1 == 0 || magnitudeV2 == 0)
{
return 0f;
}
// 计算夹角的余弦值
float cosTheta = dotProduct / (magnitudeV1 * magnitudeV2);
// 限制cosTheta的值范围在 -1 到 1 之间,以防计算机浮点误差
cosTheta = Math.Max(-1f, Math.Min(1f, cosTheta));
// 返回角度,单位是度
return (float)(Math.Acos(cosTheta) * (180.0 / Math.PI));
}
// 计算点集合的中心点(几何质心)
private static Vector3 GetCentroid(List<Vector3> vectors)
{
float x = vectors.Sum(v => v.X) / vectors.Count;
float y = vectors.Sum(v => v.Y) / vectors.Count;
float z = vectors.Sum(v => v.Z) / vectors.Count;
return new Vector3(x, y, z);
}
/// <summary>
/// 判断集合元素是狗共面
/// </summary>
/// <param name="triangle"></param>
/// <param name="points"></param>
/// <returns></returns>
public static bool ArePointsOnSamePlane(Tuple<Vector3, Vector3, Vector3> triangle, List<Vector3> points)
{
// 获取三角形的三个点
var P1 = triangle.Item1;
var P2 = triangle.Item2;
var P3 = triangle.Item3;
// 计算平面的法线
Vector3 V1 = P2 - P1; // 向量P1P2
Vector3 V2 = P3 - P1; // 向量P1P3
Vector3 normal = Vector3.Cross(V1, V2); // 叉积得到平面法线
// 对每个点进行检查
foreach (var point in points)
{
if (!IsPointInPlane(P1, normal, point))
{
return false; // 如果有点不在平面上,返回false
}
}
return true; // 如果所有点都在平面上,返回true
}
/// <summary>
/// 判断点是否在平面上
/// </summary>
/// <returns></returns>
private static bool IsPointInPlane(Vector3 planePoint, Vector3 normal, Vector3 point)
{
// 计算点到平面的距离
// 点到平面的距离公式:d = (point - planePoint) · normal / |normal|
float distance = Vector3.Dot(point - planePoint, normal) / normal.Length();
// 如果距离为0,说明点在平面上
return Math.Abs(distance) < 1e-6; // 容忍一个小的误差
}
public static void GenerateLightingForModel(Viewport3DX viewport)
{
// 移除现有的灯光
List<Light3D> lights = new List<Light3D>();
foreach (var item in viewport.Items)
{
if (item is Light3D)
{
lights.Add(item as Light3D);
}
}
lights.ForEach(item => viewport.Items.Remove(item));
// 获取场景中最大的模型
var models = viewport.Items.OfType<MeshGeometryModel3D>();
if (!models.Any()) return;
var largestModel = models
.OrderByDescending(m => GetBoundingBoxVolume(m.Geometry.Bound))
.FirstOrDefault();
if (largestModel == null) return;
// 获取最大模型的边界立方体
var boundingBox = largestModel.Geometry.Bound;
var size = boundingBox.Size;
var center = boundingBox.Center;
// 计算 Bounding Box 的八个顶点
var corners = new List<Point3D>
{
boundingBox.Minimum.ToPoint3D(), // 左下后
new Point3D(boundingBox.Maximum.X, boundingBox.Minimum.Y, boundingBox.Minimum.Z), // 右下后
new Point3D(boundingBox.Minimum.X, boundingBox.Maximum.Y, boundingBox.Minimum.Z), // 左上后
new Point3D(boundingBox.Minimum.X, boundingBox.Minimum.Y, boundingBox.Maximum.Z), // 左下前
boundingBox.Maximum.ToPoint3D(), // 右上前
new Point3D(boundingBox.Maximum.X, boundingBox.Maximum.Y, boundingBox.Minimum.Z), // 右上后
new Point3D(boundingBox.Minimum.X, boundingBox.Maximum.Y, boundingBox.Maximum.Z), // 左上前
new Point3D(boundingBox.Maximum.X, boundingBox.Minimum.Y, boundingBox.Maximum.Z) // 右下前
};
// 在每个顶点处添加光源
for (int i = 0; i < corners.Count; i++)
{
var corner = corners[i];
// 设置点光源的颜色和强度,使得八个点的光源具有不同效果
var color = i % 2 == 0 ? Colors.LightGoldenrodYellow : Colors.LightSkyBlue;
// 添加点光源
var pointLight = new PointLight3D
{
Position = corner,
Color = color,
Range = (float)size.Length() * 4 // 光照范围覆盖整个模型
};
viewport.Items.Add(pointLight);
}
// 添加顶部的多处光源(从上向下照射,模拟太阳光)
double topLightDistance = size.Y * 1.2; // 适当调整顶部光源的高度
var topLightPositions = new List<Point3D>
{
new Point3D(center.X, boundingBox.Maximum.Y + topLightDistance, center.Z),
new Point3D(boundingBox.Minimum.X - size.X / 3, boundingBox.Maximum.Y + topLightDistance, center.Z),
new Point3D(boundingBox.Maximum.X + size.X / 3, boundingBox.Maximum.Y + topLightDistance, center.Z)
};
foreach (var position in topLightPositions)
{
var topLight = new PointLight3D
{
Position = position,
Color = Colors.LightGoldenrodYellow,
Range = (float)size.Length() * 0.5, // 增加光的照射范围
Attenuation = new Vector3D(1, 0.1f, 0.05f) // 控制光的衰减,使光在距离内更有效
};
viewport.Items.Add(topLight);
}
// 添加环境光以柔化整体效果并增加亮度
var ambientLight = new AmbientLight3D
{
Color = Colors.LightGray
};
viewport.Items.Add(ambientLight);
var ambientLigh1t = new AmbientLight3D
{
Color = Colors.Gray // 设置环境光颜色
};
viewport.Items.Add(ambientLigh1t);
// 添加环境光以柔化整体效果并增加亮度
// var ambientLight = new AmbientLight3D
// {
// Color = Colors.LightGray
// };
// viewport.Items.Add(ambientLight);
// 添加底部的光源(从底部中心向上照射)
// var bottomLightPosition = new Point3D(center.X, boundingBox.Minimum.Y - 1.0f, center.Z); // 位于底部中心点下方
// var bottomLight = new PointLight3D
// {
// Position = bottomLightPosition,
// Color = Colors.White, // 白色光源
// Range = (float)size.Length() * 2 // 设置光照范围
// };
// viewport.Items.Add(bottomLight);
}
// 辅助方法:获取边界体积
private static double GetBoundingBoxVolume(BoundingBox bound)
{
var size = bound.Size;
return size.X * size.Y * size.Z;
}
public static List<Viewport3DTriangleEntity> InitDemo(Viewport3DX viewport)
{
List<Viewport3DTriangleEntity> entities = new List<Viewport3DTriangleEntity>();
try
{
using (FileStream fileStream = new FileStream(AppDomain.CurrentDomain.BaseDirectory + @"Resource\DimDemo.txt", FileMode.Open, FileAccess.Read))
using (StreamReader reader = new StreamReader(fileStream))
{
string line;
while ((line = reader.ReadLine()) != null)
{
if(string.IsNullOrWhiteSpace(line))continue;
if(line.StartsWith("-- "))continue;
entities.Add(Viewport3DTriangleEntity.CreateByString(line));
}
}
}
catch (Exception ex)
{
Console.WriteLine($"读取文件时出错:{ex.Message}");
}
GenerateModelByEntity(viewport, entities);
GenerateLightingForModel(viewport);
return entities;
}
public static List<GeometryModel3D> GenerateEmissiveModelByEntity(Viewport3DX viewport, List<Viewport3DTriangleEntity> entities, Color4 emissiveColor)
{
// 创建 MeshBuilder,启用法线生成
var meshBuilder = new MeshBuilder(true, false);
viewport.RenderHost.MSAA = MSAALevel.Maximum;
// 通过实体构建三角形面
foreach (var entity in entities)
{
List<Vector3> vertices = new List<Vector3>() { entity.Point1, entity.Point2, entity.Point3 };
meshBuilder.AddPolygon(vertices);
}
// 生成网格
var mesh = meshBuilder.ToMeshGeometry3D();
// 创建自发光材质
var material = new PBRMaterial
{
AlbedoColor = new Color4(0, 0, 0, 1.0f), // 黑色,避免其他光照影响
EmissiveColor = emissiveColor, // 自发光颜色
MetallicFactor = 0.0, // 非金属
RoughnessFactor = 1.0, // 高粗糙度,避免反射效果
ReflectanceFactor = 0.0, // 无反射
ClearCoatStrength = 0.0, // 无清漆效果
ClearCoatRoughness = 1.0, // 高粗糙度
};
// 设置材质的采样器状态
material.SurfaceMapSampler = new SamplerStateDescription
{
Filter = Filter.MinMagMipLinear,
AddressU = TextureAddressMode.Wrap,
AddressV = TextureAddressMode.Wrap,
AddressW = TextureAddressMode.Wrap
};
// 创建几何模型
var geometryModel = new MeshGeometryModel3D
{
Geometry = mesh,
Material = material
};
// 将几何模型添加到视图中
viewport.Items.Add(geometryModel);
List<GeometryModel3D> result = new List<GeometryModel3D>();
result.Add(geometryModel);
return result;
}
public static List<GeometryModel3D> GenerateLineTextModelByEntity(Viewport3DX viewport, List<Viewport3DTriangleEntity> entities)
{
List<GeometryModel3D> result = new List<GeometryModel3D>();
List<Vector3> selFaceVector = new List<Vector3>();
foreach (var entity in entities)
{
selFaceVector.Add(entity.Point1);
selFaceVector.Add(entity.Point2);
selFaceVector.Add(entity.Point3);
}
var edgeLines = new List<Tuple<Vector3, Vector3>>();
var lengths = new List<double>(); // 长度列表
var angles = new List<double>(); // 夹角列表
HashSet<Vector3> uniqueVectors = new HashSet<Vector3>(selFaceVector);
List<Vector3> sortedVectors = uniqueVectors.ToList();
Vector3 center = GetCentroid(sortedVectors);
sortedVectors.Sort((v1, v2) =>
{
double angle1 = Math.Atan2(v1.Z - center.Z, v1.X - center.X); // 计算第一个点的角度
double angle2 = Math.Atan2(v2.Z - center.Z, v2.X - center.X); // 计算第二个点的角度
return angle1.CompareTo(angle2); // 按角度从小到大排序
});
for (int i = 0; i < sortedVectors.Count - 1; i++)
{
var nowItem = sortedVectors[i];
var nextItem = sortedVectors[i + 1];
edgeLines.Add(new Tuple<Vector3, Vector3>(nowItem, nextItem));
// 计算当前线段的长度
lengths.Add((nextItem - nowItem).Length());
}
edgeLines.Add(new Tuple<Vector3, Vector3>(sortedVectors.Last(), sortedVectors.First()));
lengths.Add((sortedVectors.Last() - sortedVectors.First()).Length());
//计算夹角
for (int i = 0; i < sortedVectors.Count; i++)
{
int prevIndex = (i - 1 + sortedVectors.Count) % sortedVectors.Count;
int nextIndex = (i + 1) % sortedVectors.Count;
// 获取三点
var p1 = sortedVectors[prevIndex];
var p2 = sortedVectors[i];
var p3 = sortedVectors[nextIndex];
// 计算夹角
var v1 = p2 - p1;
var v2 = p3 - p2;
double angle = AngleBetween(v1, v2);
angles.Add(angle); // 夹角,单位是度
}
if (sortedVectors.Count == lengths.Count && sortedVectors.Count == angles.Count)
{
for (int i = 0; i < sortedVectors.Count; i++)
{
var point = sortedVectors[i];
var pointNext = i + 1 >= sortedVectors.Count ? sortedVectors.First() : sortedVectors[i + 1];
var lengthText = $"L: {lengths[i]:F2}";
var angleText = $"Angle: {angles[i]:F2}°";
var lengthTextPoint = (pointNext + point) / 2;
// 在合适位置显示文本
var text3DL = DisplayText3D(lengthText,
new Vector3(lengthTextPoint.X, lengthTextPoint.Y + 0.5f, lengthTextPoint.Z));
var text3DA = DisplayText3D(angleText, new Vector3(point.X, point.Y + 0.5f, point.Z + 0.5f));
viewport.Items.Add(text3DL);
viewport.Items.Add(text3DA);
result.Add(text3DL);
result.Add(text3DA);
}
}
else
{
// 如果列表长度不一致,给出提示信息
Console.WriteLine("The lengths or angles list size does not match the sorted vectors list.");
}
var lineBuilder = new LineBuilder();
foreach (var line in edgeLines)
{
lineBuilder.AddLine(line.Item1, line.Item2);
}
var edgeLinesModel = new LineGeometryModel3D
{
Geometry = lineBuilder.ToLineGeometry3D(),
Color = System.Windows.Media.Colors.Red,
Thickness = 1.0
};
viewport.Items.Add(edgeLinesModel);
result.Add(edgeLinesModel);
return result;
}
}

@ -3,8 +3,10 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AButtonBase_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6412d4331611499aab4eb63809a2a83bf60910_003Ffb_003F3185652c_003FButtonBase_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AButton_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6412d4331611499aab4eb63809a2a83bf60910_003F94_003F46375460_003FButton_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AButton_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F89a2b53da2e32f22f450ecab5f59ea9f03bfe76b409627069931a786fcee_003FButton_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AClrObjectRuntime_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fab21534b1ec041498b3c23a65e9ed82b15a918_003F19_003F3f4e517b_003FClrObjectRuntime_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AContentControl_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F27bb3fae2c21cbe9a2565477feaa7a5b3a19cd779b4a91a98edfff9cd468972_003FContentControl_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADependencyObject_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F60b63c019ead4a238340b47a1c0010d5226910_003Fca_003F09e9dbc0_003FDependencyObject_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEventRoute_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F96a561fe76144633acef44f09d0dcb8a825920_003Fb0_003F8f1a0289_003FEventRoute_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEventToCommand_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F436b6c3e323d68842c9e251322f5d42b47569f7c925e63aa245dc65465d2843_003FEventToCommand_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEventTriggerBase_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffdc001c928464b80ad45ffa09b838a3a15e200_003Faf_003F9abbeb44_003FEventTriggerBase_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExecutionContext_002Ecs_002Fl_003AC_0021_003FUsers_003FAdministrator_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F211e6f3b24fa438a92f1815153647ce2c8f908_003F35_003F053c62c1_003FExecutionContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
@ -16,9 +18,14 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJsonSerializer_002Ecs_002Fl_003AC_0021_003FUsers_003FAdministrator_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F7e62198beab24380bbac29171862d1d8adf10_003F9d_003Fb98e2c8a_003FJsonSerializer_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJsonTextReader_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F7e62198beab24380bbac29171862d1d8adf10_003Fcf_003F3a31e3b0_003FJsonTextReader_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJToken_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F58b0b7706a8be4f7160749424eb996a3f845a1682c3c7ac5e405a346a8c20f1_003FJToken_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMaterialVariable_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F68d37d16685244cf9996bf767117a771210200_003Fe1_003Fb1acf2fc_003FMaterialVariable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARoutedEventArgs_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F1a88b4a860176dd5f825206bbebf3ee3d44ff3f058ceed9eb693c1eaa018_003FRoutedEventArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARuntimeType_002ECoreCLR_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F955ec549fe664629353c3b5424b6ad6c7dfcec4ab59bae709ab962c228cf45_003FRuntimeType_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcca5cfb955e146648d91eb22ffe4627a84930_003F7a_003F2d86be72_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F2c8e7ca976f350cba9836d5565dac56b11e0b56656fa786460eb1395857a6fa_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATriggerAction_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F95b2fd5cb826a0d61aff88f87b258644cfe6df15959e521eb9d6cc8da70dc52_003FTriggerAction_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUIElement_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbfd90ad81939493b96034353abcd1045825908_003F4b_003F18f975e4_003FUIElement_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUndoStack_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F85eb3e3af2ef415e8072243864dec55a97e00_003F18_003F3f9ef08f_003FUndoStack_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AXamlReader_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fc82ad29b96d5485f88fa4f2ce6e6c019f60908_003F0b_003Fbba3a168_003FXamlReader_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AWindow_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd0db11e55b76dc7f234163f6cee32b297b8ddb591fb0b5cbad1b46ed17343e18_003FWindow_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AXamlReader_002Ecs_002Fl_003AC_0021_003FUsers_003Ftongg_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fc82ad29b96d5485f88fa4f2ce6e6c019f60908_003F0b_003Fbba3a168_003FXamlReader_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=9B346951_002D9ACB_002D4A13_002DB212_002D8C75AED24131_002Fd_003AViews_002Fd_003AGrading_002Ff_003AGradingResult_002Examl/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>

@ -6,8 +6,10 @@ using System.Windows.Input;
using System.Windows.Shapes;
using HandyControl.Controls;
using log4net.Appender;
using Newtonsoft.Json;
using SparkClient.Model.Entity.ApiEntity;
using SparkClient.Model.Helper;
using SparkClient.Views.UserControl.ViewportData;
using static System.Windows.Forms.VisualStyles.VisualStyleElement.ToolTip;
namespace SparkClient.ViewModel.Grading;
@ -16,9 +18,10 @@ public class GradingResultVM : BaseViewModel
{
private string DiamondCode { get; set; }
private List<DataInfo> _dtResults;
// ViewportData
private DataTable _dsList;
private DataTable _gradeList;
private ViewportData _viewportData;
private string _standard;
private string _shape;
@ -29,6 +32,7 @@ public class GradingResultVM : BaseViewModel
private string _symLevelTotal;
public ICommand ChangeSymCommand { get; }
public List<DataInfo> DtResults { get { return _dtResults; } set { _dtResults = value; OnPropertyChanged(nameof(DtResults)); } }
public ViewportData ViewportData { get { return _viewportData; } set { _viewportData = value; OnPropertyChanged(nameof(ViewportData)); } }
public string Standard { get { return _standard; } set { _standard = value; OnPropertyChanged(nameof(Standard)); } }
public string Shape { get { return _shape; } set { _shape = value; OnPropertyChanged(nameof(Shape)); } }
public string CrownType { get { return _crownType; } set { _crownType = value; OnPropertyChanged(nameof(CrownType)); } }
@ -58,6 +62,10 @@ public class GradingResultVM : BaseViewModel
}
private void InitView(AlgorithmResultEntity result)
{
string data = JsonConvert.SerializeObject(result);
ViewportData = new ViewportData(result.DiamondCode, data);
ViewportData.LoadData();
totalCutGrade = 0;
totalSymGrade = 0;
Standard = result.Standard;

@ -154,13 +154,15 @@
</DataGrid>
</Border>
<GridSplitter Grid.Row="1" Grid.Column="1" Width="3" HorizontalAlignment="Stretch"></GridSplitter>
<Border Grid.Row="1" Grid.Column="2" Background="Aquamarine">
<Border Grid.Row="1" Grid.Column="2" Background="White">
<Grid>
<Grid.RowDefinitions>
<!-- <RowDefinition Height="50"></RowDefinition> -->
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<userControl:Viewport3D ></userControl:Viewport3D>
<userControl:Viewport3D ViewportData="{Binding ViewportData}"></userControl:Viewport3D>
<!-- <Border Grid.Row="0" Background="Fuchsia" /> -->
<!-- <Border Grid.Row="1" BorderThickness="1" CornerRadius="0" BorderBrush="Black" Margin="5"> -->
<!-- <hx:Viewport3DX Grid.Column="0" x:Name="Viewport3Dx" MouseLeftButtonDown="Viewport3Dx_OnMouseLeftButtonDown" ClipToBounds="True"> -->

@ -14,7 +14,7 @@ namespace SparkClient.Views.Grading;
public partial class GradingResult
{
List<Viewport3DTriangleEntity> triangles = new List<Viewport3DTriangleEntity>();
// List<Viewport3DTriangleEntity> triangles = new List<Viewport3DTriangleEntity>();
List<GeometryModel3D> mouseAddModels = new List<GeometryModel3D>();
public GradingResult()
{

@ -6,7 +6,7 @@
xmlns:local="clr-namespace:SparkClient.Views.UserControl"
xmlns:hx="http://helix-toolkit.org/wpf/SharpDX"
xmlns:hc="https://handyorg.github.io/handycontrol"
mc:Ignorable="d"
mc:Ignorable="d" Loaded="Viewport3D_OnLoaded"
>
<!-- Tongg注: 后期如果时间充裕把这个控件单独封装在一个DLL里边,影响MVVM结构-->
<Grid>
@ -24,64 +24,78 @@
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Background="Transparent" BorderBrush="Transparent" Height="50" Padding="2">
<Border BorderBrush="DarkGoldenrod" BorderThickness="1" CornerRadius="10" Padding="5" >
<Border BorderThickness="1" CornerRadius="10" Padding="5">
<Border.Background>
<RadialGradientBrush GradientOrigin="0.5,0.5" Center="0.5,0.5" RadiusX="0.5" RadiusY="0.5">
<GradientStop Color="#FF4C4D4F" Offset="0.0" />
</RadialGradientBrush>
</Border.Background>
<Image Source="pack://Application:,,,/Resource/Images/UserControl/zuanshi@3x.png" ></Image>
</Border>
</Button>
<Button Grid.Column="1" Background="Transparent" BorderBrush="Transparent" Height="50" Padding="2">
<Border BorderBrush="DarkGoldenrod" BorderThickness="1" CornerRadius="10" Padding="5" >
<Border BorderThickness="1" CornerRadius="10" Padding="5" >
<Border.Background>
<RadialGradientBrush GradientOrigin="0.5,0.5" Center="0.5,0.5" RadiusX="0.5" RadiusY="0.5">
<GradientStop Color="#FF4C4D4F" Offset="0.0" />
</RadialGradientBrush>
</Border.Background>
<Image Source="pack://Application:,,,/Resource/Images/UserControl/Top@3x.png" ></Image>
</Border>
</Button>
<Button Grid.Column="2" Background="Transparent" BorderBrush="Transparent" Height="50" Padding="2">
<Border BorderBrush="DarkGoldenrod" BorderThickness="1" CornerRadius="10" Padding="5" >
<Border BorderThickness="1" CornerRadius="10" Padding="5" >
<Border.Background>
<RadialGradientBrush GradientOrigin="0.5,0.5" Center="0.5,0.5" RadiusX="0.5" RadiusY="0.5">
<GradientStop Color="#FF4C4D4F" Offset="0.0" />
</RadialGradientBrush>
</Border.Background>
<Image Source="pack://Application:,,,/Resource/Images/UserControl/Bottom@3x.png" ></Image>
</Border>
</Button>
<Button Grid.Column="3" Background="Transparent" BorderBrush="Transparent" Height="50" Padding="2">
<Border BorderBrush="DarkGoldenrod" BorderThickness="1" CornerRadius="10" Padding="5" >
<Border BorderThickness="1" CornerRadius="10" Padding="5" >
<Border.Background>
<RadialGradientBrush GradientOrigin="0.5,0.5" Center="0.5,0.5" RadiusX="0.5" RadiusY="0.5">
<GradientStop Color="#FF4C4D4F" Offset="0.0" />
</RadialGradientBrush>
</Border.Background>
<Image Source="pack://Application:,,,/Resource/Images/UserControl/biaochi1@3x.png" ></Image>
</Border>
</Button>
<Button Grid.Column="4" Background="Transparent" BorderBrush="Transparent" Height="50" Padding="2">
<Border BorderBrush="DarkGoldenrod" BorderThickness="1" CornerRadius="10" Padding="5" >
<Border BorderThickness="1" CornerRadius="10" Padding="5" >
<Border.Background>
<RadialGradientBrush GradientOrigin="0.5,0.5" Center="0.5,0.5" RadiusX="0.5" RadiusY="0.5">
<GradientStop Color="#FF4C4D4F" Offset="0.0" />
</RadialGradientBrush>
</Border.Background>
<Image Source="pack://Application:,,,/Resource/Images/UserControl/biaochi2@3x.png" ></Image>
</Border>
</Button>
</Grid>
<Border Grid.Row="1" x:Name="MainBorder">
<Border.ContextMenu>
<!-- <sys:String x:Key="ViewportRightMenuFront">正面视角</sys:String> -->
<!-- <sys:String x:Key="ViewportRightMenuSaveViewToPNG">截图当前视角到PNG</sys:String> -->
<!-- <sys:String x:Key="ViewportRightMenuShowModelFace">显示模型面</sys:String> -->
<!-- <sys:String x:Key="ViewportRightMenuShowModelFrame">显示模型边框</sys:String> -->
<!-- <sys:String x:Key="ViewportRightMenuShowLighting">显示光照</sys:String> -->
<!-- <sys:String x:Key="ViewportRightMenuSelectFace">双击选择面</sys:String> -->
<!-- <sys:String x:Key="ViewportRightMenuSelectFaceFrame">显示选择面的边框</sys:String> -->
<!-- <sys:String x:Key="ViewportRightMenuSelectFaceLengthText">显示选择面边框长度文本</sys:String> -->
<!-- <sys:String x:Key="ViewportRightMenuSelectFaceAngleText">显示选择面夹角角度文本</sys:String> -->
<!-- <sys:String x:Key="ViewportRightMenuSelectFaceKind">显示选择面的同类面</sys:String> -->
<!-- <sys:String x:Key="ViewportRightMenuShowDefectFace">显示瑕疵面</sys:String> -->
<!-- <sys:String x:Key="ViewportRightMenuShowFront">显示正方向标识</sys:String> -->
<ContextMenu>
<!-- 菜单 -->
<MenuItem Header="{StaticResource ViewportRightMenuFront}" Click="MenuItem_OnClick"/>
<MenuItem Header="{StaticResource ViewportRightMenuSaveViewToPNG}" Click="MenuItem_OnClick"/>
<MenuItem Header="{StaticResource ViewportRightMenuFront}" x:Name="ViewportRightMenuFront" Click="MenuItem_OnClick"/>
<MenuItem Header="{StaticResource ViewportRightMenuSaveViewToPNG}" x:Name="ViewportRightMenuSaveViewToPNG" Click="MenuItem_OnClick"/>
<!-- 选择 -->
<MenuItem Header="{StaticResource ViewportRightMenuShowModelFace}" IsCheckable="True" Checked="MenuItem_OnChecked" Unchecked="MenuItem_OnUnchecked"/>
<MenuItem Header="{StaticResource ViewportRightMenuShowModelFrame}" IsCheckable="True" Checked="MenuItem_OnChecked" Unchecked="MenuItem_OnUnchecked"/>
<MenuItem Header="{StaticResource ViewportRightMenuShowLighting}" IsCheckable="True" Checked="MenuItem_OnChecked" Unchecked="MenuItem_OnUnchecked"/>
<MenuItem Header="{StaticResource ViewportRightMenuShowDefectFace}" IsCheckable="True" Checked="MenuItem_OnChecked" Unchecked="MenuItem_OnUnchecked"/>
<MenuItem Header="{StaticResource ViewportRightMenuShowFront}" IsCheckable="True" Checked="MenuItem_OnChecked" Unchecked="MenuItem_OnUnchecked"/>
<Separator />
<MenuItem Header="{StaticResource ViewportRightMenuSelectFace}" IsCheckable="True" Checked="MenuItem_OnChecked" Unchecked="MenuItem_OnUnchecked"/>
<MenuItem Header="{StaticResource ViewportRightMenuSelectFaceFrame}" IsCheckable="True" Checked="MenuItem_OnChecked" Unchecked="MenuItem_OnUnchecked"/>
<MenuItem Header="{StaticResource ViewportRightMenuSelectFaceLengthText}" IsCheckable="True" Checked="MenuItem_OnChecked" Unchecked="MenuItem_OnUnchecked"/>
<MenuItem Header="{StaticResource ViewportRightMenuSelectFaceAngleText}" IsCheckable="True" Checked="MenuItem_OnChecked" Unchecked="MenuItem_OnUnchecked"/>
<MenuItem Header="{StaticResource ViewportRightMenuSelectFaceKind}" IsCheckable="True" Checked="MenuItem_OnChecked" Unchecked="MenuItem_OnUnchecked"/>
<MenuItem Header="{StaticResource ViewportRightMenuShowModelFace}" x:Name="ViewportRightMenuShowModelFace" IsCheckable="True" Checked="MenuItem_OnChecked" Unchecked="MenuItem_OnUnchecked"/>
<MenuItem Header="{StaticResource ViewportRightMenuShowModelFrame}" x:Name="ViewportRightMenuShowModelFrame" IsCheckable="True" Checked="MenuItem_OnChecked" Unchecked="MenuItem_OnUnchecked"/>
<MenuItem Header="{StaticResource ViewportRightMenuShowLighting}" x:Name="ViewportRightMenuShowLighting" IsCheckable="True" Checked="MenuItem_OnChecked" Unchecked="MenuItem_OnUnchecked"/>
<MenuItem Header="{StaticResource ViewportRightMenuShowDefectFace}" x:Name="ViewportRightMenuShowDefectFace" IsCheckable="True" Checked="MenuItem_OnChecked" Unchecked="MenuItem_OnUnchecked"/>
<MenuItem Header="{StaticResource ViewportRightMenuShowFront}" x:Name="ViewportRightMenuShowFront" IsCheckable="True" Checked="MenuItem_OnChecked" Unchecked="MenuItem_OnUnchecked"/>
<Separator />
<MenuItem Header="{StaticResource ViewportRightMenuSelectFace}" x:Name="ViewportRightMenuSelectFace" IsCheckable="True" Checked="MenuItem_OnChecked" Unchecked="MenuItem_OnUnchecked"/>
<MenuItem Header="{StaticResource ViewportRightMenuSelectFaceFrame}" x:Name="ViewportRightMenuSelectFaceFrame" IsCheckable="True" Checked="MenuItem_OnChecked" Unchecked="MenuItem_OnUnchecked"/>
<MenuItem Header="{StaticResource ViewportRightMenuSelectFaceLengthText}" x:Name="ViewportRightMenuSelectFaceLengthText" IsCheckable="True" Checked="MenuItem_OnChecked" Unchecked="MenuItem_OnUnchecked"/>
<MenuItem Header="{StaticResource ViewportRightMenuSelectFaceAngleText}" x:Name="ViewportRightMenuSelectFaceAngleText" IsCheckable="True" Checked="MenuItem_OnChecked" Unchecked="MenuItem_OnUnchecked"/>
<MenuItem Header="{StaticResource ViewportRightMenuSelectFaceKind}" x:Name="ViewportRightMenuSelectFaceKind" IsCheckable="True" Checked="MenuItem_OnChecked" Unchecked="MenuItem_OnUnchecked"/>
</ContextMenu>
</Border.ContextMenu>
<hx:Viewport3DX ShowCoordinateSystem="True" PreviewMouseRightButtonUp="UIElement_OnPreviewMouseRightButtonUp" PreviewMouseRightButtonDown="UIElement_OnPreviewMouseRightButtonDown">
<hx:Viewport3DX x:Name="Viewport3Dx" ShowCoordinateSystem="True" PreviewMouseRightButtonUp="UIElement_OnPreviewMouseRightButtonUp" PreviewMouseRightButtonDown="UIElement_OnPreviewMouseRightButtonDown">
<hx:Viewport3DX.EffectsManager><hx:EffectsManager ></hx:EffectsManager></hx:Viewport3DX.EffectsManager>
<hx:Viewport3DX.InputBindings>
<KeyBinding Key="B" Command="hx:ViewportCommands.BackView" />

@ -1,18 +1,161 @@
using System.Text.Json.Nodes;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using HandyControl.Controls;
using HelixToolkit.Wpf.SharpDX;
using SharpDX;
using SparkClient.Views.UserControl.ViewportData.Entity;
using SparkClient.Views.UserControl.ViewportData.Enum;
using SparkClient.Views.UserControl.ViewportData.Helper;
namespace SparkClient.Views.UserControl;
public partial class Viewport3D
{
// 注册一个依赖属性,类型是 Viewport3DData
public static readonly DependencyProperty ViewportDataProperty =
DependencyProperty.Register(
nameof(ViewportData),
typeof(ViewportData.ViewportData),
typeof(Viewport3D),
new PropertyMetadata(null, OnViewportDataChanged));
// 公开的 CLR 属性包装器
public ViewportData.ViewportData ViewportData
{
get => (ViewportData.ViewportData)GetValue(ViewportDataProperty);
set => SetValue(ViewportDataProperty, value);
}
// 当 ViewportData 属性发生变化时触发
private static void OnViewportDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is Viewport3D control && e.NewValue is ViewportData.ViewportData data)
{
control.Initialize(data);
}
}
private void Initialize(ViewportData.ViewportData vpData)
{
// 根据 Viewport3DData 的内容初始化控件
// 例如:设置控件内部的某些属性,渲染 3D 模型等
if (!string.IsNullOrEmpty(vpData.DiamondData))
{
JsonNode? json = null;
try
{
json = JsonNode.Parse(vpData.DiamondData);
}
catch
{
throw new Exception("DiamondData is invalid");
}
if(json == null) throw new Exception("DiamondData is invalid");
try
{
List<Viewport3DTriangleEntity> facets = new List<Viewport3DTriangleEntity>();
if (json.AsObject()["facets"] != null)
{
JsonArray jsonArray = json.AsObject()["facets"].AsArray();
foreach (var item in jsonArray)
{
var value = CommonHelper.CreateByJsonStr(item.ToString());
facets.Add(value);
}
}
else
{
throw new Exception("facets is invalid");
}
if (json.AsObject()["positive_direction"] != null)
{
JsonObject positive = json.AsObject()["positive_direction"].AsObject();
ViewportManager.PositiveDirection.X = float.Parse(positive["x"]?.ToString() ?? "1.0");
ViewportManager.PositiveDirection.Y = float.Parse(positive["z"]?.ToString() ?? "0");
ViewportManager.PositiveDirection.Z = float.Parse(positive["y"]?.ToString() ?? "0");
}
else
{
ViewportManager.PositiveDirection.X = 1.0f;
ViewportManager.PositiveDirection.Y = 0f;
ViewportManager.PositiveDirection.Z = 0;
}
var midZ = facets.SelectMany(e => new[] { e.Point1.X, e.Point2.X, e.Point3.X }).OrderBy(z => Math.Abs(z))
.First();
List<Viewport3DTriangleEntity> facetsFinal = new List<Viewport3DTriangleEntity>();
foreach (var item in facets)
{
var data = ViewportHelperPro.VectorClockwiseSort(new List<Vector3>
{ item.Point1, item.Point2, item.Point3 });
if (item.PlaneType == PlaneType.StarFacet || item.PlaneType == PlaneType.UpperGirdleFacet ||
item.PlaneType == PlaneType.UpperMainFacet || item.PlaneType == PlaneType.TableFacet)
{
item.Point1 = data[0];
item.Point2 = data[1];
item.Point3 = data[2];
}
else if (item.PlaneType == PlaneType.Girdle)
{
var center = ViewportHelperPro.GetCentroid(data);
data.Sort((v1, v2) =>
{
double angle1 = Math.Atan2(v1.Y - center.Y, v1.X - center.X);
double angle2 = Math.Atan2(v2.Y - center.Y, v2.X - center.X);
return angle1.CompareTo(angle2);
});
if (center.Z >= midZ)
{
item.Point1 = data[0];
item.Point2 = data[1];
item.Point3 = data[2];
}
else
{
item.Point1 = data[2];
item.Point2 = data[1];
item.Point3 = data[0];
}
}
else
{
item.Point1 = data[2];
item.Point2 = data[1];
item.Point3 = data[0];
}
item.TriangleCode = CommonHelper.GenerateTriangleCode(item.Point1, item.Point2, item.Point3);
facetsFinal.Add(item);
}
// ViewportManager.ViewportTriangle.AddRange(facetsFinal);
ViewportManager.LoadModelByEntities(facetsFinal);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
}
}
public Viewport3D()
{
InitializeComponent();
DataContext = this;
this.Viewport3Dx.EffectsManager = new DefaultEffectsManager();
ViewportManager.SetViewport3D(Viewport3Dx);
}
#region 菜单事件绑定
private void MenuItem_OnChecked(object sender, RoutedEventArgs e)
{
Growl.InfoGlobal("Viewport is checked" + e.OriginalSource);
@ -22,7 +165,13 @@ public partial class Viewport3D
{
Growl.InfoGlobal("Viewport is Unchecked" + e.OriginalSource);
}
private void MenuItem_OnClick(object sender, RoutedEventArgs e)
{
}
#endregion
#region 页面隐式交互
private void UIElement_OnPreviewMouseRightButtonUp(object sender, MouseButtonEventArgs e)
{
MainBorder.ContextMenu.IsOpen = true;
@ -33,9 +182,11 @@ public partial class Viewport3D
MainBorder.ContextMenu.IsOpen = false;
e.Handled = true;
}
#endregion
private void MenuItem_OnClick(object sender, RoutedEventArgs e)
private void Viewport3D_OnLoaded(object sender, RoutedEventArgs e)
{
//ViewportData.LoadData();
ViewportManager.LoadModelByEntities(new List<Viewport3DTriangleEntity>());
}
}

@ -0,0 +1,6 @@
namespace SparkClient.Views.UserControl.ViewportData.Entity;
public class MeasurementsDataEntity
{
}

@ -0,0 +1,41 @@
using SharpDX;
using SparkClient.Views.UserControl.ViewportData.Enum;
namespace SparkClient.Views.UserControl.ViewportData.Entity;
/// <summary>
/// 三角形模型实体
/// </summary>
public class Viewport3DTriangleEntity
{
/// <summary>
/// 点1
/// </summary>
public Vector3 Point1 { get; set; }
/// <summary>
/// 点2
/// </summary>
public Vector3 Point2 { get; set; }
/// <summary>
/// 点3
/// </summary>
public Vector3 Point3 { get; set; }
/// <summary>
/// 三角形代码[生成]
/// 按顺序:p1.x,p1.y,p1.z;p2.x,p2.y,p2.z;p3.x,p3.y,p3.z 拼接后使用生成大写16位md5
/// </summary>
public String TriangleCode { get; set; }
/// <summary>
/// 面代码
/// 由多个三角形组成的面的代码:entity1、entity2组成了一个正方形,那么他俩的TriangleCode属性值一致
/// </summary>
public String PlaneCode { get; set; }
/// <summary>
/// 面类型
/// 比如这个面时钻石的腰部分,那么type统一,当面类型为err时则该面是一个异常面
/// 可选值:PlaneType
/// </summary>
public PlaneType PlaneType { get; set; }
}

@ -0,0 +1,31 @@
using System.ComponentModel;
namespace SparkClient.Views.UserControl.ViewportData.Enum;
public enum PlaneType
{
[Description("瑕疵面")]
Error = -1,
[Description("腰")]
Girdle = 0,
[Description("冠部")]
Crown = 1,
[Description("亭部")]
Pavilion = 2,
[Description("台面")]
TableFacet = 11,
[Description("冠部主刻面(风筝面)")]
UpperMainFacet = 12,
[Description("星刻面")]
StarFacet = 13,
[Description("上腰面")]
UpperGirdleFacet = 14,
[Description("亭部主刻面")]
PavilionMainFacet = 21,
[Description("下腰面")]
LowerGirdleFact = 22,
[Description("底尖(或底小面)")]
Culet = 23,
[Description("其他")]
Other = 99
}

@ -0,0 +1,78 @@
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;
using SharpDX;
using SparkClient.Views.UserControl.ViewportData.Entity;
using SparkClient.Views.UserControl.ViewportData.Enum;
namespace SparkClient.Views.UserControl.ViewportData.Helper;
public class CommonHelper
{
public static Viewport3DTriangleEntity CreateByJsonStr(string json)
{
// 解析 JSON 数据
var jsonObject = JsonConvert.DeserializeObject<dynamic>(json);
if (jsonObject == null) throw new ArgumentException("Json object is null");
// 提取坐标
var coords = jsonObject.coords.ToObject<dynamic[]>();
if (coords.Length < 3)
{
throw new ArgumentException("The input JSON does not have enough points to form a triangle.");
}
// 创建三角形顶点
var point1 = new Vector3((float)coords[0].x, (float)coords[0].z, (float)coords[0].y);
var point2 = new Vector3((float)coords[1].x, (float)coords[1].z, (float)coords[1].y);
var point3 = new Vector3((float)coords[2].x, (float)coords[2].z, (float)coords[2].y);
// var point1 = new Vector3((float)coords[0].x, (float)coords[0].y, (float)coords[0].z);
// var point2 = new Vector3((float)coords[1].x, (float)coords[1].y, (float)coords[1].z);
// var point3 = new Vector3((float)coords[2].x, (float)coords[2].y, (float)coords[2].z);
// 提取 PlaneCode 和 PlaneType
string planeCode = jsonObject.facet_id ?? (string)jsonObject.facet_id;
PlaneType planeType = (PlaneType)(int)jsonObject.facet_type;
// 生成 TriangleCode
var triangleCode = GenerateTriangleCode(point1, point2, point3);
// 创建并返回实体对象
return new Viewport3DTriangleEntity
{
Point1 = point1,
Point2 = point2,
Point3 = point3,
TriangleCode = triangleCode,
PlaneCode = planeCode,
PlaneType = planeType
};
}
/// <summary>
/// 生成三角形签名
/// </summary>
/// <param name="p1"></param>
/// <param name="p2"></param>
/// <param name="p3"></param>
/// <returns></returns>
public static string GenerateTriangleCode(Vector3 p1, Vector3 p2, Vector3 p3)
{
var concatenated = $"{p1.X},{p1.Y},{p1.Z};{p2.X},{p2.Y},{p2.Z};{p3.X},{p3.Y},{p3.Z}";
return GenerateMd5Hash(concatenated);
}
/// <summary>
/// 文本转32大写MD5
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
private static string GenerateMd5Hash(string input)
{
using (var md5 = MD5.Create())
{
var inputBytes = Encoding.UTF8.GetBytes(input);
var hashBytes = md5.ComputeHash(inputBytes);
return string.Concat(hashBytes.Select(b => b.ToString("X2")));
}
}
}

@ -0,0 +1,873 @@
using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using HelixToolkit.Wpf.SharpDX;
using SharpDX;
using SharpDX.Direct3D11;
using SparkClient.Views.UserControl.ViewportData.Enum;
using SparkClient.Views.UserControl.ViewportData.Entity;
using Color = SharpDX.Color;
using GeometryModel3D = HelixToolkit.Wpf.SharpDX.GeometryModel3D;
namespace SparkClient.Views.UserControl.ViewportData.Helper;
public class ViewportHelperPro
{
/// <summary>
/// 对指定类型的面进行标色
/// </summary>
/// <param name="planeType"></param>
/// <param name="color"></param>
/// <returns></returns>
public static MeshGeometryModel3D GenerateTypePanelHot(PlaneType planeType, Color4? color = null)
{
var entities = ViewportManager.ViewportTriangle.Where(e=>e.PlaneType==planeType).ToList();
return GenerateModelByEntity(entities, color??ViewportManager.Red);
}
#region 已经确定和调整好的方法
/// <summary>
/// 通过三角形实体集合生成面模型(生成并添加)
/// </summary>
/// <param name="viewport"></param>
/// <param name="entities"></param>
public static MeshGeometryModel3D GenerateModelByEntity(Viewport3DX viewport, List<Viewport3DTriangleEntity> entities, Color4? color = null)
{
var geometryModel = GenerateModelByEntity(entities, color);
viewport.Items.Add(geometryModel);
return geometryModel;
}
/// <summary>
/// 通过三角形实体集合生成面模型(只生成不添加)
/// </summary>
/// <param name="entities"></param>
/// <param name="color"></param>
/// <returns></returns>
public static MeshGeometryModel3D GenerateModelByEntity(List<Viewport3DTriangleEntity> entities, Color4? color = null)
{
var meshBuilder = new MeshBuilder(true, false);
foreach (var entity in entities)
{
meshBuilder.AddPolygon(new List<Vector3>() { entity.Point1, entity.Point2, entity.Point3 });
}
var mesh = meshBuilder.ToMeshGeometry3D();
var material = new PBRMaterial
{
AlbedoColor = ViewportManager.Black, // 黑色,避免其他光照影响
EmissiveColor =color ?? ViewportManager.LightGray , // LightGray #D3D3D3
MetallicFactor = 0.0, // 非金属
RoughnessFactor = 1.0, // 高粗糙度,避免反射效果
ReflectanceFactor = 0.0, // 无反射
ClearCoatStrength = 0.0, // 无清漆效果
ClearCoatRoughness = 1.0, // 高粗糙度
SurfaceMapSampler = new SamplerStateDescription
{
Filter = Filter.MinMagMipLinear,
AddressU = TextureAddressMode.Wrap,
AddressV = TextureAddressMode.Wrap,
AddressW = TextureAddressMode.Wrap
},
// NormalMap = texture
};
return new MeshGeometryModel3D
{
Geometry = mesh,
Material = material,
};
}
/// <summary>
/// 保存模型截图
/// </summary>
/// <param name="viewport"></param>
/// <param name="filePath"></param>
public static void SaveViewportAsImage(Viewport3DX viewport, string filePath)
{
// 定义图像大小
int width = (int)viewport.ActualWidth;
int height = (int)viewport.ActualHeight;
// 创建 RenderTargetBitmap,捕获 Viewport3DX 的内容
var renderTargetBitmap = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32);
renderTargetBitmap.Render(viewport);
// 使用 PngBitmapEncoder 保存为 PNG 文件
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(renderTargetBitmap));
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
encoder.Save(fileStream);
}
}
/// <summary>
/// 导出模型
/// </summary>
/// <param name="viewport"></param>
/// <param name="filePath"></param>
public static void ExportModelsToStl(Viewport3DX viewport, string filePath)
{
// 打开文件写入流
using (StreamWriter writer = new StreamWriter(filePath))
{
writer.WriteLine("solid exportedModel");
foreach (var model in viewport.Items.OfType<MeshGeometryModel3D>())
{
if (model.Geometry is HelixToolkit.Wpf.SharpDX.Geometry3D geometry)
{
var positions = geometry.Positions;
var indices = geometry.Indices;
// 每三个索引构成一个三角形
for (int i = 0; i < indices.Count; i += 3)
{
int index0 = indices[i];
int index1 = indices[i + 1];
int index2 = indices[i + 2];
var p0 = positions[index0];
var p1 = positions[index1];
var p2 = positions[index2];
// 计算法线
var normal = CalculateNormal(p0, p1, p2);
// 写入三角形信息到 STL 文件
writer.WriteLine($" facet normal {normal.X} {normal.Z} {normal.Y}");
writer.WriteLine(" outer loop");
writer.WriteLine($" vertex {p0.X} {p0.Z} {p0.Y}");
writer.WriteLine($" vertex {p1.X} {p1.Z} {p1.Y}");
writer.WriteLine($" vertex {p2.X} {p2.Z} {p2.Y}");
writer.WriteLine(" endloop");
writer.WriteLine(" endfacet");
}
}
}
// 写入 STL 文件结束
writer.WriteLine("endsolid exportedModel");
}
}
/// <summary>
/// 通过三角形实体集合生成每个面的边框线
/// </summary>
/// <param name="viewport"></param>
/// <param name="entities"></param>
public static List<LineGeometryModel3D> GentrateLineByEntity(Viewport3DX viewport, List<Viewport3DTriangleEntity> entities, Color4? color = null, double thickness = 1.0)
{
List<LineGeometryModel3D> result = GentrateLineByEntity(entities, color, thickness);
result.ForEach(e => viewport.Items.Add(e));
return result;
}
/// <summary>
/// 通过三角形实体集合生成每个面的边框线(只生成不添加)
/// </summary>
/// <param name="entities"></param>
/// <param name="color"></param>
/// <param name="thickness"></param>
/// <returns></returns>
public static List<LineGeometryModel3D> GentrateLineByEntity(List<Viewport3DTriangleEntity> entities, Color4? color = null, double thickness = 1.0)
{
List<LineGeometryModel3D> result = new List<LineGeometryModel3D>();
//按面分组,腰面特殊单独生成
List<Viewport3DTriangleEntity> waistList = entities
.Where(entity => entity.PlaneType == PlaneType.Girdle)
.ToList();
Dictionary<string, List<Viewport3DTriangleEntity>> feactList = entities
.Where(entity => entity.PlaneType != PlaneType.Girdle)
.GroupBy(entity => entity.PlaneCode)
.ToDictionary(group => group.Key, group => group.ToList());
foreach (var item in feactList)
{
//GenerateLineTextModelByEntity(viewport, item.Value, Viewport3DManager.Gray);
List<Vector3> temp = new List<Vector3>();
foreach (var entity in item.Value)
{
temp.Add(entity.Point1);
temp.Add(entity.Point2);
temp.Add(entity.Point3);
}
result.Add(DisplayLineModel3D(VectorClockwiseSort(new HashSet<Vector3>(temp).ToList()), color??ViewportManager.LineColor, thickness));
}
//腰 - 特殊
List<Vector3> selFaceVector = new List<Vector3>();
if (waistList.Count > 0)
{
foreach (var entity in waistList)
{
selFaceVector.Add(entity.Point1);
selFaceVector.Add(entity.Point2);
selFaceVector.Add(entity.Point3);
}
HashSet<Vector3> uniqueVectors = new HashSet<Vector3>(selFaceVector);
float maxY = uniqueVectors.Max(v => v.Y);
float minY = uniqueVectors.Min(v => v.Y);
float mid = (maxY + minY) / 2;
ViewportManager.GirdleTopLines.Clear();
ViewportManager.GirdleBottomLines.Clear();
foreach (var vector in uniqueVectors)
{
if(vector.Y < mid)
ViewportManager.GirdleBottomLines.Add(vector);
else
ViewportManager.GirdleTopLines.Add(vector);
}
result.Add(DisplayLineModel3D( VectorClockwiseSort(ViewportManager.GirdleBottomLines), color??ViewportManager.LineColor, thickness));
result.Add(DisplayLineModel3D( VectorClockwiseSort(ViewportManager.GirdleTopLines), color??ViewportManager.LineColor, thickness));
}
return result;
}
/// <summary>
/// 显示线段长度mm
/// </summary>
/// <param name="entities">三角形集合</param>
/// <param name="textColor">文字颜色</param>
/// <param name="showAll">是否显示全部(false:只显示不重复)</param>
/// <returns></returns>
public static List<GeometryModel3D> GenerateLineTextModels(List<Viewport3DTriangleEntity> entities,
Color4? textColor = null,bool showAll = false)
{
var result = new List<GeometryModel3D>();
var selFaceVector = entities
.SelectMany(entity => new[] { entity.Point1, entity.Point2, entity.Point3 })
.Distinct()
.ToList();
var uniqueLines = new HashSet<string>();
var sortedVectors = VectorClockwiseSort(selFaceVector);
for (int i = 0; i < sortedVectors.Count; i++)
{
var current = sortedVectors[i];
var next = sortedVectors[(i + 1) % sortedVectors.Count];
double length = (next - current).Length();
string lineKey = $"{length:F2}";
if (showAll == false)
{
if (uniqueLines.Contains(lineKey)) continue;
uniqueLines.Add(lineKey);
}
var midPoint = (current + next) / 2;
var text = $"{length:F2}mm";
var textY = midPoint.Y > ViewportManager.CenterVector.Y ? midPoint.Y + 0.3f : midPoint.Y - 0.3f;
var lengthTextModel = DisplayText3D(text, new Vector3(midPoint.X, textY, midPoint.Z),next - current, textColor);
result.Add(lengthTextModel);
}
return result;
}
/// <summary>
/// 生成选择的视图()
/// </summary>
/// <param name="triangleCode">三角形ID</param>
/// <param name="selType">生成范围</param>
/// <returns></returns>
public static List<GeometryModel3D> GentrateChosenView(string triangleCode, params SelShowType[] selType)
{
var res = ViewportManager.ViewportTriangle.Find(e => triangleCode.Equals(e.TriangleCode));
if(res != null)
return GentrateChosenView(res, selType);
return new List<GeometryModel3D>();
}
/// <summary>
/// 生成选择的视图(只生成,不添加!!)
/// </summary>
/// <param name="entity">选中的实体</param>
/// <param name="selType">生成范围</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static List<GeometryModel3D> GentrateChosenView(Viewport3DTriangleEntity entity, params SelShowType[] selType)
{
if (entity == null || entity.TriangleCode == null || entity.TriangleCode.Length == 0)
{
throw new Exception("Invalid Viewport3DTriangleEntity object!");
}
if (selType == null || selType.Length == 0)
{
throw new Exception("Invalid SelShowType collection");
}
List<GeometryModel3D> result = new List<GeometryModel3D>();
//选中的面
var selPanel = ViewportManager.ViewportTriangle.Where(e => entity.PlaneCode.Equals(e.PlaneCode)).ToList();
//选中同类的面
var selPanelType = ViewportManager.ViewportTriangle.Where(e => entity.PlaneType.Equals(e.PlaneType)).ToList();
selPanel.ForEach(e=>selPanelType.Remove(e));
foreach (var type in selType)
{
switch (type)
{
case SelShowType.SelPanel:
//选中面的高亮
result.Add(GenerateModelByEntity(selPanel, ViewportManager.DarkKhaki));
break;
case SelShowType.Border:
//选中面边框的高亮
result.AddRange(GentrateLineByEntity(selPanel, ViewportManager.Black));
break;
case SelShowType.IsTypePanel:
//选中面的同类面高亮
result.Add(GenerateModelByEntity(selPanelType, ViewportManager.LtGoldenrodYello));
break;
case SelShowType.LengthText:
//选中面 每条边长度标记
if(PlaneType.Girdle == entity.PlaneType)break;
result.AddRange(GenerateLineTextModels(selPanel));
break;
case SelShowType.BorderAngle:
//选中面 每条边向内的夹角
if(PlaneType.Girdle == entity.PlaneType)break;
result.AddRange(GenerateLineAngleTextModels(selPanel));
break;
}
}
//生成的需要添加的元素
return result;
}
/// <summary>
/// 生成线段夹角文本(不绘制)
/// </summary>
/// <param name="entities">线集合</param>
/// <param name="textColor">文本颜色(默认红)</param>
/// <param name="showAll">是否显示全部(默认否)</param>
/// <returns></returns>
public static List<GeometryModel3D> GenerateLineAngleTextModels(List<Viewport3DTriangleEntity> entities,
Color4? textColor = null, bool showAll = false)
{
var result = new List<GeometryModel3D>();
var uniqueAngles = new HashSet<string>();
var selFaceVector = entities
.SelectMany(entity => new[] { entity.Point1, entity.Point2, entity.Point3 })
.Distinct()
.ToList();
var sortedVectors = VectorClockwiseSort(selFaceVector);
for (int i = 0; i < sortedVectors.Count; i++)
{
int prevIndex = (i - 1 + sortedVectors.Count) % sortedVectors.Count;
int nextIndex = (i + 1) % sortedVectors.Count;
var p1 = sortedVectors[prevIndex];
var p2 = sortedVectors[i];
var p3 = sortedVectors[nextIndex];
// 向量
var v1 = Vector3.Normalize(p1 - p2);
var v2 = Vector3.Normalize(p3 - p2);
// 计算内角(考虑方向性)
double angle = Math.Acos(Vector3.Dot(v1, v2)) * (180 / Math.PI);
string angleKey = $"{angle:F2}"; // 唯一标识
if (showAll == false)
{
if (uniqueAngles.Contains(angleKey)) continue;
uniqueAngles.Add(angleKey);
}
// 显示内角
var text = $"{angle:F2}°";
var textY = p2.Y > ViewportManager.CenterVector.Y ? p2.Y + 0.3f : p2.Y - 0.3f;
var angleTextModel = DisplayText3D(text, new Vector3(p2.X, textY, p2.Z), textColor);
result.Add(angleTextModel);
}
return result;
}
#endregion
/// <summary>
/// 旋转主体
/// </summary>
/// <param name="axis">中心</param>
/// <param name="hasLine">是否包含线</param>
/// <param name="speed">旋转时间 秒</param>
public static void RotateModel(Vector3D axis, bool hasLine = true, int speed = 5)
{
// 设置旋转的中心点和旋转轴
var rotateTransform = new RotateTransform3D();
var rotation = new AxisAngleRotation3D(axis, 0); // 初始角度为 0
rotateTransform.Rotation = rotation;
rotateTransform.CenterX = ViewportManager.CenterVector.X;
rotateTransform.CenterY = ViewportManager.CenterVector.Y;
rotateTransform.CenterZ = ViewportManager.CenterVector.Z;
// 将旋转变换应用到模型
ViewportManager.MainModel3D.Transform = rotateTransform;
if(hasLine)
ViewportManager.MainModelLines.ForEach(e => e.Transform = rotateTransform);
// 创建旋转动画
var rotateAnimation = new DoubleAnimation
{
From = 0,
To = 360,
Duration = new Duration(TimeSpan.FromSeconds(speed)),
FillBehavior = FillBehavior.HoldEnd
};
// 应用动画
rotation.BeginAnimation(AxisAngleRotation3D.AngleProperty, rotateAnimation);
}
/// <summary>
/// 在场景中添加文字
/// </summary>
/// <param name="text">文字</param>
/// <param name="position">位置</param>
/// <returns></returns>
public static BillboardTextModel3D DisplayText3D(string text, Vector3 position, Color4? color = null)
{
var billboardTextModel = new BillboardTextModel3D();
var billboardText = new BillboardText3D();
billboardText.TextInfo.Add(new TextInfo(text, position)
{
Foreground = color??ViewportManager.Red,
Scale = 0.5f,
});
billboardTextModel.Geometry = billboardText;
return billboardTextModel;
}
/// <summary>
/// 在场景中添加文字
/// </summary>
/// <param name="text"></param>
/// <param name="position"></param>
/// <param name="direction"></param>
/// <param name="color"></param>
/// <returns></returns>
public static BillboardTextModel3D DisplayText3D(string text, Vector3 position, Vector3 direction, Color4? color = null)
{
// 计算线段方向的角度
double angle = Math.Atan2(direction.Y, direction.X) * (180 / Math.PI);
var billboardTextModel = new BillboardTextModel3D()
{
Geometry = new BillboardText3D()
{
TextInfo = new ObservableCollection<TextInfo>()
{
new TextInfo(text, position){
Foreground = color ?? ViewportManager.Red,
Scale = 0.5f,
Offset = new Vector2(0.2f, 0.2f),
// Angle = (float)angle // 设置文字的旋转角度
}
}
},
DepthBias = -99,
SlopeScaledDepthBias = 1.0f,
IsDepthClipEnabled = true
};
return billboardTextModel;
}
/// <summary>
/// 生成线对象
/// </summary>
/// <param name="points">点集合(自动闭环)</param>
/// <param name="lineColor">线颜色</param>
/// <param name="thickness">线粗细 默认1</param>
/// <returns></returns>
public static LineGeometryModel3D DisplayLineModel3D(List<Vector3> points, Color4 lineColor,
double thickness = 1.0)
{
var edgeLines = new List<Tuple<Vector3, Vector3>>();
for (int i = 0; i < points.Count - 1; i++)
{
var nowItem = points[i];
var nextItem = points[i + 1];
edgeLines.Add(new Tuple<Vector3, Vector3>(nowItem, nextItem));
}
edgeLines.Add(new Tuple<Vector3, Vector3>(points.Last(), points.First()));
return DisplayLineModel3D(edgeLines, lineColor, thickness);
}
/// <summary>
/// 在正方向生成相机
/// </summary>
/// <param name="positiveDirection"></param>
/// <param name="boundingBox"></param>
/// <param name="fieldOfView"></param>
/// <param name="aspectRatio"></param>
/// <returns></returns>
public static HelixToolkit.Wpf.SharpDX.PerspectiveCamera CalculateCamera(Vector3 positiveDirection, BoundingBox boundingBox, double fieldOfView = 45, double aspectRatio = 16.0 / 9.0)
{
// 单位化正方向
var normalizedDirection = Vector3.Normalize(positiveDirection);
// 计算模型中心和最大尺寸
var center = boundingBox.Center; // 包围盒中心
var maxDimension = boundingBox.Size.Length(); // 包围盒对角线长度
// 计算视场角的一半(弧度制)
var halfFov = Math.PI * fieldOfView / 360.0;
// 计算相机距离
var distance = maxDimension / (2 * Math.Tan(halfFov));
// 相机位置和朝向
var cameraPosition = center - normalizedDirection * (float)distance; // 沿正方向放置相机
var lookDirection = center - cameraPosition; // 朝向模型中心
// 创建并返回相机
return new HelixToolkit.Wpf.SharpDX.PerspectiveCamera
{
Position = cameraPosition.ToPoint3D(),
LookDirection = lookDirection.ToVector3D(),
UpDirection = new Vector3D(0, 1, 0), // 默认全局 Y 轴为上方向
FieldOfView = fieldOfView,
};
}
/// <summary>
/// 绘制箭头(相机指向模型中心)
/// </summary>
/// <param name="cameraPosition">相机位置</param>
/// <param name="modelCenter">中心位置</param>
/// <param name="modelBounds">模型</param>
/// <param name="totalLength">箭头总长度 D:5</param>
/// <param name="cylinderRatio">圆柱部分占比 0.7</param>
/// <param name="diameter">箭头直径 1</param>
/// <param name="headDiameterRatio">圆锥直径与圆柱直径的比例 1.5</param>
/// <param name="padding">模型边界范围 1</param>
/// <returns></returns>
public static MeshGeometryModel3D CreateArrow(
Vector3 cameraPosition,
Vector3 modelCenter,
BoundingBox modelBounds,
float totalLength = 2f, // 箭头总长度
float cylinderRatio = 0.7f,// 圆柱部分占比
float diameter = 0.5f, // 箭头直径
float headDiameterRatio = 1f, // 圆锥直径与圆柱直径的比例
float padding = 2.0f
)
{
// 计算模型的半径(对角线的一半)
var modelSize = modelBounds.Size;
var modelRadius = modelSize.Length() / 2;
// 计算方向向量(从相机位置指向模型中心)
var direction = Vector3.Normalize(modelCenter - cameraPosition);
// 调整箭头起点,避免与模型交集
var adjustedStart = modelCenter - direction * (modelRadius + padding);
// 计算圆柱和圆锥部分长度
float cylinderLength = totalLength * cylinderRatio;
float coneLength = totalLength - cylinderLength;
// 箭头圆柱终点
var cylinderEnd = adjustedStart + direction * cylinderLength;
// 箭头终点(圆锥部分终点)
var arrowEnd = cylinderEnd + direction * coneLength;
// 创建箭头几何体
var builder = new MeshBuilder();
// 添加圆柱部分
builder.AddCylinder(adjustedStart, cylinderEnd, diameter, 20);
// 添加圆锥部分
builder.AddCone(cylinderEnd, arrowEnd, diameter * headDiameterRatio, true, 20);
// 设置箭头自发光材质
var material = new PhongMaterial
{
DiffuseColor = Color.Red, // 箭头的基本颜色
EmissiveColor = Color.Red // 箭头自发光颜色
};
// 创建 3D 模型
return new MeshGeometryModel3D
{
Geometry = builder.ToMeshGeometry3D(),
Material = material
};
}
/// <summary>
/// 生成线对象
/// </summary>
/// <param name="points">线段集合</param>
/// <param name="lineColor">线段颜色</param>
/// <param name="thickness">线段粗细</param>
/// <returns></returns>
public static LineGeometryModel3D DisplayLineModel3D(List<Tuple<Vector3, Vector3>> points, Color4 lineColor,
double thickness = 1.0)
{
var lineBuilder = new LineBuilder();
foreach (var line in points)
{
lineBuilder.AddLine(line.Item1, line.Item2);
}
var edgeLinesModel = new LineGeometryModel3D
{
Geometry = lineBuilder.ToLineGeometry3D(),
Color = lineColor.ToColor(),
Thickness = thickness,
};
return edgeLinesModel;
}
/// <summary>
/// 根据视图中模型生成光照
/// </summary>
/// <param name="viewport"></param>
/// <returns></returns>
public static List<Light3D> GenerateLightingForModel(Viewport3DX viewport)
{
List<Light3D> result = new List<Light3D>();
var models = viewport.Items.OfType<MeshGeometryModel3D>();
if (!models.Any()) throw new Exception("Model in view not found");
var largestModel = models
.OrderByDescending(m => GetBoundingBoxVolume(m.Geometry.Bound))
.FirstOrDefault();
if (largestModel == null) throw new Exception("Model in view not found");
var boundingBox = largestModel.Geometry.Bound;
var size = boundingBox.Size;
var center = boundingBox.Center;
ViewportManager.CenterVector = center;
ViewportManager.ModelBounds = boundingBox;
var yao = CalculateCenter(ViewportManager.GirdleTopLines, ViewportManager.GirdleBottomLines);
var corners = new List<Point3D>
{
new Point3D(boundingBox.Minimum.X-0.5, boundingBox.Minimum.Y-boundingBox.Maximum.Y/2, boundingBox.Minimum.Z-0.5), // 右下后
new Point3D(boundingBox.Maximum.X+0.5, boundingBox.Minimum.Y-boundingBox.Maximum.Y/2, boundingBox.Minimum.Z-0.5), // 右下后
new Point3D(boundingBox.Minimum.X-0.5, boundingBox.Minimum.Y-boundingBox.Maximum.Y/2, boundingBox.Maximum.Z+0.5), // 左下前
new Point3D(boundingBox.Maximum.X+0.5, boundingBox.Minimum.Y-boundingBox.Maximum.Y/2, boundingBox.Maximum.Z+0.5), // 右下前
// new Point3D(boundingBox.Minimum.X-1, yao.Y, boundingBox.Minimum.Z-1), // 右下后
// new Point3D(boundingBox.Maximum.X+1, yao.Y, boundingBox.Minimum.Z-1), // 右下后
// new Point3D(boundingBox.Minimum.X-1, yao.Y, boundingBox.Maximum.Z+1), // 左下前
// new Point3D(boundingBox.Maximum.X+1, yao.Y, boundingBox.Maximum.Z+1), // 右下前
new Point3D(boundingBox.Minimum.X-0.5, boundingBox.Maximum.Y+boundingBox.Maximum.Y/4, boundingBox.Minimum.Z-0.5), // 左上后
new Point3D(boundingBox.Maximum.X+0.5, boundingBox.Maximum.Y+boundingBox.Maximum.Y/4,boundingBox.Maximum.Z+0.5), // 右上前
new Point3D(boundingBox.Maximum.X+0.5, boundingBox.Maximum.Y+boundingBox.Maximum.Y/4, boundingBox.Minimum.Z-0.5), // 右上后
new Point3D(boundingBox.Minimum.X-0.5, boundingBox.Maximum.Y+boundingBox.Maximum.Y/4, boundingBox.Maximum.Z+0.5), // 左上前
};
for (int i = 0; i < corners.Count; i++)
{
var corner = corners[i];
var color = i % 2 == 0 ? Colors.LightGoldenrodYellow : Colors.LightSkyBlue;
var pointLight = new PointLight3D
{
Position = corner,
Color = color,
Range = (float)size.Length()
};
result.Add(pointLight);
}
// var topLightPositions = new List<Point3D>
// {
// new Point3D(center.X, center.Y, center.Z),
// };
// foreach (var position in topLightPositions)
// {
// var topLight = new PointLight3D
// {
// Position = position,
// Color = Colors.LightGoldenrodYellow,
// Range = (float)size.Length() * 0.5, // 增加光的照射范围
// Attenuation = new Vector3D(1, 0.1f, 0.05f) // 控制光的衰减,使光在距离内更有效
// };
// result.Add(topLight);
// }
// 添加环境光以柔化整体效果并增加亮度
result.Add(new AmbientLight3D
{
Color = Colors.LightGray
});
result.Add(new AmbientLight3D
{
Color = Colors.Gray // 设置环境光颜色
});
RemoveLightingInViewport(viewport);
result.ForEach(e => viewport.Items.Add(e));
return result;
}
public static Vector3 GetCenterOfTriangles(List<Viewport3DTriangleEntity> triangles)
{
if (triangles == null || triangles.Count == 0)
throw new ArgumentException("The list of triangles cannot be null or empty.");
// 累加所有顶点的坐标
Vector3 total = Vector3.Zero;
int vertexCount = 0;
foreach (var triangle in triangles)
{
total += triangle.Point1;
total += triangle.Point2;
total += triangle.Point3;
vertexCount += 3; // 每个三角形有3个顶点
}
// 计算平均坐标
return total / vertexCount;
}
public static Vector3 CalculateCenter(List<Vector3> girdleTopLines, List<Vector3> girdleBottomLines)
{
// 计算 GirdleTopLines 的中心点
Vector3 topCenter = GetCenter(girdleTopLines);
// 计算 GirdleBottomLines 的中心点
Vector3 bottomCenter = GetCenter(girdleBottomLines);
// 计算两个中心点的中点作为垂直方向的中心点
Vector3 centerInVertical = new Vector3(
(topCenter.X + bottomCenter.X) / 2,
(topCenter.Y + bottomCenter.Y) / 2,
(topCenter.Z + bottomCenter.Z) / 2
);
return centerInVertical;
}
/// <summary>
/// 删除视图中的照明
/// </summary>
/// <param name="viewport"></param>
public static void RemoveLightingInViewport(Viewport3DX viewport)
{
List<Light3D> lights = new List<Light3D>();
foreach (var item in viewport.Items)
{
if (item is Light3D light3D)
{
lights.Add(light3D);
}
}
lights.ForEach(item => viewport.Items.Remove(item));
}
#region 私有方法
/// <summary>
/// 向量按中心点顺时针排序
/// </summary>
/// <param name="points"></param>
/// <returns></returns>
public static List<Vector3> VectorClockwiseSort(List<Vector3> points)
{
Vector3 center = GetCentroid(points);
return VectorClockwiseSort(points, center);
}
/// <summary>
/// 向量按中心点顺时针排序
/// </summary>
/// <param name="points"></param>
/// <param name="center"></param>
/// <returns></returns>
public static List<Vector3> VectorClockwiseSort(List<Vector3> points, Vector3 center)
{
points.Sort((v1, v2) =>
{
double angle1 = Math.Atan2(v1.Z - center.Z, v1.X - center.X);
double angle2 = Math.Atan2(v2.Z - center.Z, v2.X - center.X);
return angle1.CompareTo(angle2);
});
return points;
}
private static Vector3 GetCenter(List<Vector3> points)
{
float x = points.Average(p => p.X);
float y = points.Average(p => p.Y);
float z = points.Average(p => p.Z);
return new Vector3(x, y, z);
}
/// <summary>
/// 计算点集合的中心点(几何质心)
/// </summary>
/// <param name="vectors"></param>
/// <returns></returns>
public static Vector3 GetCentroid(List<Vector3> vectors)
{
float x = vectors.Sum(v => v.X) / vectors.Count;
float y = vectors.Sum(v => v.Y) / vectors.Count;
float z = vectors.Sum(v => v.Z) / vectors.Count;
return new Vector3(x, y, z);
}
/// <summary>
/// 计算夹角度数
/// </summary>
/// <param name="v1"></param>
/// <param name="v2"></param>
/// <returns></returns>
private static float AngleBetween(Vector3 v1, Vector3 v2)
{
// 计算两个向量的点积
float dotProduct = Vector3.Dot(v1, v2);
// 计算每个向量的模长
float magnitudeV1 = v1.Length();
float magnitudeV2 = v2.Length();
// 防止除以零错误
if (magnitudeV1 == 0 || magnitudeV2 == 0)
{
return 0f;
}
// 计算夹角的余弦值
float cosTheta = dotProduct / (magnitudeV1 * magnitudeV2);
// 限制cosTheta的值范围在 -1 到 1 之间,以防计算机浮点误差
cosTheta = Math.Max(-1f, Math.Min(1f, cosTheta));
// 返回角度,单位是度
return (float)(Math.Acos(cosTheta) * (180.0 / Math.PI));
}
private static double GetBoundingBoxVolume(BoundingBox bound)
{
var size = bound.Size;
return size.X * size.Y * size.Z;
}
private static Vector3 CalculateNormal(Vector3 p0, Vector3 p1, Vector3 p2)
{
var u = p1 - p0;
var v = p2 - p0;
return Vector3.Cross(u, v);
}
#endregion
}

@ -0,0 +1,355 @@
using HelixToolkit.Wpf.SharpDX;
using SharpDX;
using SharpDX.Direct3D11;
using SparkClient.Views.UserControl.ViewportData.Entity;
using SparkClient.Views.UserControl.ViewportData.Enum;
namespace SparkClient.Views.UserControl.ViewportData.Helper;
public class ViewportManager
{
/// <summary>
/// 视图中三角形实体们
/// </summary>
public static List<Viewport3DTriangleEntity> ViewportTriangle = new List<Viewport3DTriangleEntity>();
/// <summary>
/// 模型正方向(从模型中心出发的方向)
/// </summary>
public static Vector3 PositiveDirection = new Vector3(0, 0, 0);
/// <summary>
/// 模型中心点
/// </summary>
public static Vector3 CenterVector = new Vector3(0, 0, 0);
/// <summary>
/// 腰的顶部线
/// </summary>
public static List<Vector3> GirdleTopLines = new List<Vector3>();
/// <summary>
/// 腰的底部线
/// </summary>
public static List<Vector3> GirdleBottomLines = new List<Vector3>();
/// <summary>
/// 模型盒子
/// </summary>
public static BoundingBox ModelBounds = new BoundingBox();
/// <summary>
/// 模型主体
/// </summary>
public static MeshGeometryModel3D MainModel3D = null;
/// <summary>
/// 模型边框线
/// </summary>
public static List<LineGeometryModel3D> MainModelLines = new List<LineGeometryModel3D>();
/// <summary>
/// 模型光照
/// </summary>
public static List<Light3D> MainModelLighting = new List<Light3D>();
/// <summary>
/// 模型控件对象映射
/// </summary>
private static Viewport3DX _viewport;
public static void SetViewport3D(Viewport3DX viewport3D)
{
if (_viewport == null)
_viewport = viewport3D;
}
#region 模型选中交互管理
//是否双击选中
public static bool DoubleClickSelect = true;
//开启选中边框线
public static bool DoubleClickSelectShowBorder = true;
//选中边框长度文字
public static bool DoubleClickSelectShowBorderLength = false;
//选中边框夹角文字
public static bool DoubleClickSelectShowBorderAngle = false;
//选中同类面
public static bool DoubleClickSelectShowPlaneType = true;
//选中三角形代码
public static string ChooseTriangleCode = string.Empty;
//通过选中添加的元素
public static List<GeometryModel3D> ChooseAddModels = new List<GeometryModel3D>();
/// <summary>
/// 重置选中的面
/// </summary>
public static void ResetChooseAddModels()
{
if (string.IsNullOrWhiteSpace(ChooseTriangleCode))
{
return;
}
ClearDicModels();
HashSet<SelShowType> models = new HashSet<SelShowType>();
models.Add(SelShowType.SelPanel);
if(DoubleClickSelectShowBorder) models.Add(SelShowType.Border);
if (DoubleClickSelectShowBorderLength) models.Add(SelShowType.LengthText);
if (DoubleClickSelectShowBorderAngle) models.Add(SelShowType.BorderAngle);
if (DoubleClickSelectShowPlaneType) models.Add(SelShowType.IsTypePanel);
ChooseAddModels.AddRange( ViewportHelperPro.GentrateChosenView(ChooseTriangleCode, models.ToArray()));
ChooseAddModels.ForEach(e => _viewport.Items.Add(e));
}
/// <summary>
/// 清除所有的选中效果
/// </summary>
public static void ClearDicModels()
{
foreach (var item in ChooseAddModels)
{
_viewport.Items.Remove(item);
}
ChooseAddModels.Clear();
}
#endregion
#region 统一控制方法
public static MeshGeometryModel3D PointTowardsTheFrontModel = new MeshGeometryModel3D();
public static void PointTowardsTheFront(bool isPoint)
{
if (isPoint)
{
var camera = ViewportHelperPro.CalculateCamera(PositiveDirection, ModelBounds);
PointTowardsTheFrontModel = ViewportHelperPro.CreateArrow(camera.Position.ToVector3(), ModelBounds.Center, ModelBounds);
_viewport.Items.Add(PointTowardsTheFrontModel);
}
else
{
_viewport.Items.Remove(PointTowardsTheFrontModel);
}
}
public static MeshGeometryModel3D MarkFacesModel = new MeshGeometryModel3D();
public static void MarkSpecificFaces(bool isMark)
{
if (isMark)
{
MarkFacesModel = ViewportHelperPro.GenerateTypePanelHot(PlaneType.Error);
_viewport.Items.Add(MarkFacesModel);
}
else
{
_viewport.Items.Remove(MarkFacesModel);
}
}
/// <summary>
/// 初始化加载模型
/// </summary>
/// <param name="viewport"></param>
/// <param name="entities"></param>
public static void LoadModelByEntities(List<Viewport3DTriangleEntity> entities)
{
if (entities.Count == 0 && ViewportTriangle.Count != 0)
{
entities.AddRange(ViewportTriangle);
}
ViewportTriangle.Clear();
ViewportTriangle.AddRange(entities);
MainModel3D = ViewportHelperPro.GenerateModelByEntity(_viewport, entities);
MainModelLines = ViewportHelperPro.GentrateLineByEntity(_viewport, entities);
MainModelLighting = ViewportHelperPro.GenerateLightingForModel(_viewport);
//切换相机视角
_viewport.Camera = ViewportHelperPro.CalculateCamera(PositiveDirection, ModelBounds);
_viewport.RenderHost.MSAA = MSAALevel.Maximum;
}
/// <summary>
/// 是否显示主体模型
/// </summary>
/// <param name="isShow"></param>
public static void ShowMainModel3D(bool isShow)
{
if (MainModel3D == null) return;
if (_viewport == null) return;
if (isShow)
{
if(_viewport.Items.Contains(MainModel3D))
return;
else
_viewport.Items.Add(MainModel3D);
}
else
{
_viewport.Items.Remove(MainModel3D);
}
}
public static void ShowMainModelLines(bool isShow)
{
if (_viewport == null) return;
if (isShow)
{
MainModelLines.ForEach(e =>
{
if(!_viewport.Items.Contains(e))
_viewport.Items.Add(e);
});
}
else
{
MainModelLines.ForEach(e => _viewport.Items.Remove(e));
}
}
public static void ShowMainModelLighting(bool isShow)
{
if (_viewport == null) return;
if (isShow)
{
MainModelLighting.ForEach(e =>
{
if(!_viewport.Items.Contains(e))
_viewport.Items.Add(e);
});
}
else
{
MainModelLighting.ForEach(e => _viewport.Items.Remove(e));
}
}
public static void Translucent(bool isShow)
{
if (_viewport == null) return;
if (isShow)
{
MainModel3D.Material = _glassMaterial;
}
else
{
MainModel3D.Material = _material;
}
}
public static void TranslucentTex(bool isShow)
{
if (_viewport == null) return;
if (isShow)
{
string texturePath2 = "pack://Appliction:,,,/Res/morning_racing_circuit_16k.dds";
var texture2 = TextureModel.Create(texturePath2);
var material = new PBRMaterial
{
AlbedoColor = new SharpDX.Color4(0.0f, 1.0f, 1.0f, 1.0f), // 白色基色
MetallicFactor = 0.0f,
RoughnessFactor = 0.5f,
ReflectanceFactor = 0.2f,
RenderEnvironmentMap = false,
// AlbedoMap = texture,
IrradianceMap = texture2,
RenderIrradianceMap = true,
};
MainModel3D.Material = material;
}
else
{
MainModel3D.Material = _material;
}
}
public static void LockCameraView(bool isShow)
{
/*Generate Ruler
* FixedPosition
* WalkAround
* Inspect()
*/
// Viewport3Dx.CameraMode = CameraMode.Inspect;
if (_viewport == null) return;
if (isShow)
{
_viewport.CameraMode = CameraMode.FixedPosition;
}
else
{
_viewport.CameraMode = CameraMode.Inspect;
}
}
#endregion
#region 预制颜色
/// <summary>
/// 模型面颜色
/// </summary>
public static Color4 LightGray = new Color4(0.7372f, 0.8235f, 0.93f, 1.0f);
/// <summary>
/// 模型面边框颜色
/// </summary>
public static Color4 LineColor = new Color4(0.4117f, 0.3490f, 0.8039f, 1.0f);
/// <summary>
/// 选中面高亮色
/// </summary>
public static Color4 DarkKhaki = new Color4(0.7411f, 0.71764f, 0.4196f, 1.0f);
/// <summary>
/// 选中面同类面的高亮色
/// </summary>
public static Color4 LtGoldenrodYello = new Color4(0.98039f, 0.98039f, 0.813529f, 1.0f);
/// <summary>
/// 选中面高亮的边框色
/// </summary>
public static Color4 Black = new Color4(0f, 0f, 0f, 1.0f);
/// <summary>
/// 文字颜色
/// </summary>
public static Color4 SandyBrown = new Color4(0.95686f, 0.64313f, 0.37647f, 1.0f);
public static Color4 Red = new Color4(1f, 0.0f, 0.0f, 1.0f);
/// <summary>
/// 材质:半透明蓝
/// </summary>
private static PBRMaterial _glassMaterial = new PBRMaterial
{
// 半透明蓝色
AlbedoColor = new Color4(0.0f, 0.0f, 1.0f, 0.5f),
// 设置为非金属
MetallicFactor = 0.0,
// 光滑表面
RoughnessFactor = 0.05,
// 环境光遮蔽
AmbientOcclusionFactor = 1.0,
// 强反射
ReflectanceFactor = 0.9,
// 清漆效果(类似折射效果)
ClearCoatStrength = 0.8,
ClearCoatRoughness = 0.05,
// 启用环境贴图以增强反射
RenderEnvironmentMap = true,
// 启用阴影效果
RenderShadowMap = true
};
/// <summary>
/// 默认灰色自发光
/// </summary>
private static PBRMaterial _material = new PBRMaterial
{
AlbedoColor = Black, // 黑色,避免其他光照影响
EmissiveColor = LightGray , // LightGray #D3D3D3
MetallicFactor = 0.0, // 非金属
RoughnessFactor = 1.0, // 高粗糙度,避免反射效果
ReflectanceFactor = 0.0, // 无反射
ClearCoatStrength = 0.0, // 无清漆效果
ClearCoatRoughness = 1.0, // 高粗糙度
SurfaceMapSampler = new SamplerStateDescription
{
Filter = Filter.MinMagMipLinear,
AddressU = TextureAddressMode.Wrap,
AddressV = TextureAddressMode.Wrap,
AddressW = TextureAddressMode.Wrap
}
};
#endregion
}
public enum SelShowType
{
Border = 0,
LengthText = 1,
BorderAngle = 2,
IsTypePanel = 3,
SelPanel = 4,
}

@ -0,0 +1,143 @@
using System.Text.Json.Nodes;
using SharpDX;
using SparkClient.Views.UserControl.ViewportData.Entity;
using SparkClient.Views.UserControl.ViewportData.Enum;
using SparkClient.Views.UserControl.ViewportData.Helper;
namespace SparkClient.Views.UserControl.ViewportData;
public class ViewportData
{
/// <summary>
/// 钻石编码
/// </summary>
public String DiamondCode { get; set; }
/// <summary>
/// 钻石数据
/// </summary>
public String DiamondData { get; set; }
/// <summary>
/// 初始化构造
/// </summary>
/// <param name="diamondCode">钻石编码</param>
/// <param name="diamondData">钻石数据</param>
public ViewportData(string diamondCode, string diamondData)
{
DiamondCode = diamondCode;
DiamondData = diamondData;
}
/// <summary>
/// 空白构造
/// </summary>
public ViewportData()
{
}
public void LoadData()
{
JsonNode? json = null;
try
{
json = JsonNode.Parse(DiamondData);
}
catch
{
throw new Exception("DiamondData is invalid");
}
if(json == null) throw new Exception("DiamondData is invalid");
try
{
List<Viewport3DTriangleEntity> facets = new List<Viewport3DTriangleEntity>();
if (json.AsObject()["facets"] != null)
{
JsonArray jsonArray = json.AsObject()["facets"].AsArray();
foreach (var item in jsonArray)
{
var value = CommonHelper.CreateByJsonStr(item.ToString());
facets.Add(value);
}
}
else
{
throw new Exception("facets is invalid");
}
if (json.AsObject()["positive_direction"] != null)
{
JsonObject positive = json.AsObject()["positive_direction"].AsObject();
ViewportManager.PositiveDirection.X = float.Parse(positive["x"]?.ToString() ?? "1.0");
ViewportManager.PositiveDirection.Y = float.Parse(positive["z"]?.ToString() ?? "0");
ViewportManager.PositiveDirection.Z = float.Parse(positive["y"]?.ToString() ?? "0");
}
else
{
ViewportManager.PositiveDirection.X = 1.0f;
ViewportManager.PositiveDirection.Y = 0f;
ViewportManager.PositiveDirection.Z = 0;
}
var midZ = facets.SelectMany(e => new[] { e.Point1.X, e.Point2.X, e.Point3.X }).OrderBy(z => Math.Abs(z))
.First();
List<Viewport3DTriangleEntity> facetsFinal = new List<Viewport3DTriangleEntity>();
foreach (var item in facets)
{
var data = ViewportHelperPro.VectorClockwiseSort(new List<Vector3>
{ item.Point1, item.Point2, item.Point3 });
if (item.PlaneType == PlaneType.StarFacet || item.PlaneType == PlaneType.UpperGirdleFacet ||
item.PlaneType == PlaneType.UpperMainFacet || item.PlaneType == PlaneType.TableFacet)
{
item.Point1 = data[0];
item.Point2 = data[1];
item.Point3 = data[2];
}
else if (item.PlaneType == PlaneType.Girdle)
{
var center = ViewportHelperPro.GetCentroid(data);
data.Sort((v1, v2) =>
{
double angle1 = Math.Atan2(v1.Y - center.Y, v1.X - center.X);
double angle2 = Math.Atan2(v2.Y - center.Y, v2.X - center.X);
return angle1.CompareTo(angle2);
});
if (center.Z >= midZ)
{
item.Point1 = data[0];
item.Point2 = data[1];
item.Point3 = data[2];
}
else
{
item.Point1 = data[2];
item.Point2 = data[1];
item.Point3 = data[0];
}
}
else
{
item.Point1 = data[2];
item.Point2 = data[1];
item.Point3 = data[0];
}
item.TriangleCode = CommonHelper.GenerateTriangleCode(item.Point1, item.Point2, item.Point3);
facetsFinal.Add(item);
}
ViewportManager.ViewportTriangle.AddRange(facetsFinal);
// ViewportManager.LoadModelByEntities(facetsFinal);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
}
}
Loading…
Cancel
Save