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(); 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(); 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(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); } }