279 lines
11 KiB
C#
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);
|
|
}
|
|
}
|