Files
2026-07-13 12:49:17 +08:00

279 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.Reflection;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Helpers;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Tools.ProBuilder
{
internal static class ProBuilderMeshUtils
{
internal static object CenterPivot(JObject @params)
{
var pbMesh = ManageProBuilder.RequireProBuilderMesh(@params);
var positionsProp = ManageProBuilder._proBuilderMeshType.GetProperty("positions");
var positions = positionsProp?.GetValue(pbMesh) as System.Collections.IList;
if (positions == null || positions.Count == 0)
return new ErrorResponse("Could not read vertex positions.");
// Compute local-space bounds center
var min = (Vector3)positions[0];
var max = min;
foreach (Vector3 pos in positions)
{
min = Vector3.Min(min, pos);
max = Vector3.Max(max, pos);
}
var localCenter = (min + max) * 0.5f;
if (localCenter.sqrMagnitude < 0.0001f)
return new SuccessResponse("Pivot is already centered", new { offset = new[] { 0f, 0f, 0f } });
Undo.RecordObject(pbMesh, "Center Pivot");
Undo.RecordObject(pbMesh.transform, "Center Pivot");
// Offset all vertices by -localCenter
var newPositions = new Vector3[positions.Count];
for (int i = 0; i < positions.Count; i++)
newPositions[i] = (Vector3)positions[i] - localCenter;
// Set positions via property setter
SetVertexPositions(pbMesh, newPositions);
// Move transform to compensate
var worldOffset = pbMesh.transform.TransformVector(localCenter);
pbMesh.transform.position += worldOffset;
ManageProBuilder.RefreshMesh(pbMesh);
return new SuccessResponse("Pivot centered to mesh bounds center", new
{
offset = new[] { Round(localCenter.x), Round(localCenter.y), Round(localCenter.z) },
newPosition = new[]
{
Round(pbMesh.transform.position.x),
Round(pbMesh.transform.position.y),
Round(pbMesh.transform.position.z),
},
});
}
internal static object FreezeTransform(JObject @params)
{
var pbMesh = ManageProBuilder.RequireProBuilderMesh(@params);
var positionsProp = ManageProBuilder._proBuilderMeshType.GetProperty("positions");
var positions = positionsProp?.GetValue(pbMesh) as System.Collections.IList;
if (positions == null || positions.Count == 0)
return new ErrorResponse("Could not read vertex positions.");
Undo.RecordObject(pbMesh, "Freeze Transform");
Undo.RecordObject(pbMesh.transform, "Freeze Transform");
// Transform each vertex to world space, then back to identity local space
var worldPositions = new Vector3[positions.Count];
for (int i = 0; i < positions.Count; i++)
worldPositions[i] = pbMesh.transform.TransformPoint((Vector3)positions[i]);
// Reset transform
pbMesh.transform.position = Vector3.zero;
pbMesh.transform.rotation = Quaternion.identity;
pbMesh.transform.localScale = Vector3.one;
// Set new positions (now in world space = new local space since identity)
SetVertexPositions(pbMesh, worldPositions);
ManageProBuilder.RefreshMesh(pbMesh);
return new SuccessResponse("Transform frozen into vertex data", new
{
vertexCount = worldPositions.Length,
});
}
internal static object ValidateMesh(JObject @params)
{
var pbMesh = ManageProBuilder.RequireProBuilderMesh(@params);
var positionsProp = ManageProBuilder._proBuilderMeshType.GetProperty("positions");
var positions = positionsProp?.GetValue(pbMesh) as System.Collections.IList;
var allFaces = ManageProBuilder.GetFacesArray(pbMesh);
var facesList = (System.Collections.IList)allFaces;
int degenerateCount = 0;
var indexesProp = ManageProBuilder._faceType.GetProperty("indexes");
if (indexesProp != null && positions != null)
{
foreach (var face in facesList)
{
var indexes = indexesProp.GetValue(face) as System.Collections.IList;
if (indexes == null) continue;
// Check triangles in groups of 3
for (int i = 0; i + 2 < indexes.Count; i += 3)
{
var p0 = (Vector3)positions[(int)indexes[i]];
var p1 = (Vector3)positions[(int)indexes[i + 1]];
var p2 = (Vector3)positions[(int)indexes[i + 2]];
var area = Vector3.Cross(p1 - p0, p2 - p0).magnitude * 0.5f;
if (area < 1e-6f)
degenerateCount++;
}
}
}
// Check for unused vertices
var usedVertices = new HashSet<int>();
if (indexesProp != null)
{
foreach (var face in facesList)
{
var indexes = indexesProp.GetValue(face) as System.Collections.IList;
if (indexes == null) continue;
foreach (int idx in indexes)
usedVertices.Add(idx);
}
}
int totalVertices = positions?.Count ?? 0;
int unusedVertices = totalVertices - usedVertices.Count;
var issues = new List<string>();
if (degenerateCount > 0)
issues.Add($"{degenerateCount} degenerate triangle(s)");
if (unusedVertices > 0)
issues.Add($"{unusedVertices} unused vertex/vertices");
return new SuccessResponse(
issues.Count == 0 ? "Mesh is clean" : $"Found {issues.Count} issue type(s)",
new
{
healthy = issues.Count == 0,
faceCount = facesList.Count,
vertexCount = totalVertices,
degenerateTriangles = degenerateCount,
unusedVertices,
issues,
});
}
internal static object SetPivot(JObject @params)
{
var pbMesh = ManageProBuilder.RequireProBuilderMesh(@params);
var props = ManageProBuilder.ExtractProperties(@params);
var posToken = props["position"] ?? props["worldPosition"] ?? props["world_position"];
if (posToken == null)
return new ErrorResponse("position parameter is required ([x,y,z] in world space).");
var worldPosition = VectorParsing.ParseVector3OrDefault(posToken);
Undo.RecordObject(pbMesh, "Set Pivot");
Undo.RecordObject(pbMesh.transform, "Set Pivot");
// SetPivot moves the transform without moving the geometry visually.
// We need to offset vertex positions by the inverse of the transform change.
var positionsProp = ManageProBuilder._proBuilderMeshType.GetProperty("positions");
var positions = positionsProp?.GetValue(pbMesh) as System.Collections.IList;
if (positions == null || positions.Count == 0)
return new ErrorResponse("Could not read vertex positions.");
// Calculate delta in local space
var worldDelta = worldPosition - pbMesh.transform.position;
var localDelta = pbMesh.transform.InverseTransformVector(worldDelta);
// Offset all vertices by -localDelta to keep them in place visually
var newPositions = new Vector3[positions.Count];
for (int i = 0; i < positions.Count; i++)
newPositions[i] = (Vector3)positions[i] - localDelta;
SetVertexPositions(pbMesh, newPositions);
// Move transform to new pivot position
pbMesh.transform.position = worldPosition;
ManageProBuilder.RefreshMesh(pbMesh);
return new SuccessResponse("Pivot set to world position", new
{
position = new[] { Round(worldPosition.x), Round(worldPosition.y), Round(worldPosition.z) },
});
}
private static void SetVertexPositions(Component pbMesh, Vector3[] positions)
{
var positionsProp = ManageProBuilder._proBuilderMeshType.GetProperty("positions");
if (positionsProp != null && positionsProp.CanWrite)
positionsProp.SetValue(pbMesh, new List<Vector3>(positions));
}
internal static object RepairMesh(JObject @params)
{
var pbMesh = ManageProBuilder.RequireProBuilderMesh(@params);
Undo.RecordObject(pbMesh, "Repair Mesh");
int repaired = 0;
// Try MeshValidation.RemoveDegenerateTriangles
if (ManageProBuilder._meshValidationType != null)
{
var removeMethod = ManageProBuilder._meshValidationType.GetMethod("RemoveDegenerateTriangles",
BindingFlags.Static | BindingFlags.Public);
if (removeMethod != null)
{
var allFaces = ManageProBuilder.GetFacesArray(pbMesh);
try
{
var result = removeMethod.Invoke(null, new object[] { pbMesh, allFaces });
if (result is int count)
repaired = count;
}
catch
{
// Some overloads differ; try without faces param
try
{
var altMethod = ManageProBuilder._meshValidationType.GetMethod("RemoveDegenerateTriangles",
BindingFlags.Static | BindingFlags.Public,
null,
new[] { ManageProBuilder._proBuilderMeshType },
null);
if (altMethod != null)
{
var result = altMethod.Invoke(null, new object[] { pbMesh });
if (result is int count)
repaired = count;
}
}
catch
{
// Ignore fallback failure
}
}
}
}
ManageProBuilder.RefreshMesh(pbMesh);
return new SuccessResponse(
repaired > 0 ? $"Repaired {repaired} degenerate triangle(s)" : "No repairs needed",
new
{
degenerateTrianglesRemoved = repaired,
faceCount = ManageProBuilder.GetFaceCount(pbMesh),
vertexCount = ManageProBuilder.GetVertexCount(pbMesh),
});
}
private static float Round(float v) => ManageProBuilder.Round(v);
}
}