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

2524 lines
110 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Helpers;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Runtime.Helpers;
namespace MCPForUnity.Editor.Tools.ProBuilder
{
/// <summary>
/// Tool for managing Unity ProBuilder meshes for in-editor 3D modeling.
/// Requires com.unity.probuilder package to be installed.
///
/// SHAPE CREATION:
/// - create_shape: Create ProBuilder primitive with real dimensions via Generate* methods
/// Shape types: Cube, Cylinder, Sphere, Plane, Cone, Torus, Pipe, Arch, Stair, CurvedStair, Door, Prism
/// Each shape accepts type-specific parameters (radius, height, steps, segments, etc.)
/// - create_poly_shape: Create from 2D polygon footprint (points, extrudeHeight, flipNormals)
///
/// MESH EDITING:
/// - extrude_faces: Extrude faces (faceIndices, distance, method: FaceNormal/VertexNormal/IndividualFaces)
/// - extrude_edges: Extrude edges (edgeIndices or edges [{a,b},...], distance, asGroup)
/// - bevel_edges: Bevel edges (edgeIndices or edges [{a,b},...], amount 0-1)
/// - subdivide: Subdivide faces (faceIndices optional)
/// - delete_faces: Delete faces (faceIndices)
/// - bridge_edges: Bridge two open edges (edgeA, edgeB as {a,b} pairs, allowNonManifold)
/// - connect_elements: Connect edges/faces (edgeIndices or faceIndices)
/// - detach_faces: Detach faces (faceIndices, deleteSourceFaces)
/// - flip_normals: Flip face normals (faceIndices)
/// - merge_faces: Merge faces into one (faceIndices)
/// - combine_meshes: Combine ProBuilder objects (targets list)
/// - merge_objects: Merge objects (auto-converts non-ProBuilder), convenience wrapper (targets, name)
/// - duplicate_and_flip: Create double-sided geometry (faceIndices)
/// - create_polygon: Connect existing vertices into a new face (vertexIndices, unordered)
///
/// VERTEX OPERATIONS:
/// - merge_vertices: Collapse vertices to single point (vertexIndices, collapseToFirst)
/// - weld_vertices: Weld vertices within proximity radius (vertexIndices, radius)
/// - split_vertices: Split shared vertices (vertexIndices)
/// - move_vertices: Translate vertices (vertexIndices, offset [x,y,z])
/// - insert_vertex: Insert vertex on edge or face (edge {a,b} or faceIndex + point [x,y,z])
/// - append_vertices_to_edge: Insert evenly-spaced points on edges (edgeIndices or edges, count)
///
/// SELECTION:
/// - select_faces: Select faces by criteria (direction, growAngle, floodAngle, loop, ring)
///
/// UV &amp; MATERIALS:
/// - set_face_material: Assign material to faces (faceIndices, materialPath)
/// - set_face_color: Set vertex color (faceIndices, color [r,g,b,a])
/// - set_face_uvs: Set UV params (faceIndices, scale, offset, rotation, flipU, flipV)
///
/// QUERY:
/// - get_mesh_info: Get mesh details (face count, vertex count, bounds, materials, edges with positions)
/// - convert_to_probuilder: Convert standard mesh to ProBuilder
/// </summary>
[McpForUnityTool("manage_probuilder", AutoRegister = false, Group = "probuilder")]
public static class ManageProBuilder
{
// ProBuilder types resolved via reflection (optional package)
internal static Type _proBuilderMeshType;
private static Type _shapeGeneratorType;
internal static Type _shapeTypeEnum;
private static Type _extrudeMethodEnum;
private static Type _extrudeElementsType;
private static Type _bevelType;
private static Type _deleteElementsType;
private static Type _appendElementsType;
private static Type _connectElementsType;
private static Type _mergeElementsType;
private static Type _combineMeshesType;
private static Type _surfaceTopologyType;
internal static Type _faceType;
internal static Type _edgeType;
private static Type _editorMeshUtilityType;
private static Type _meshImporterType;
internal static Type _smoothingType;
internal static Type _meshValidationType;
private static Type _pivotLocationType;
private static Type _vertexEditingType;
private static Type _elementSelectionType;
private static Type _axisEnum;
private static bool _typesResolved;
private static bool _proBuilderAvailable;
private static bool EnsureProBuilder()
{
if (_typesResolved) return _proBuilderAvailable;
_typesResolved = true;
_proBuilderMeshType = Type.GetType("UnityEngine.ProBuilder.ProBuilderMesh, Unity.ProBuilder");
if (_proBuilderMeshType == null)
{
_proBuilderAvailable = false;
return false;
}
_shapeGeneratorType = Type.GetType("UnityEngine.ProBuilder.ShapeGenerator, Unity.ProBuilder");
_shapeTypeEnum = Type.GetType("UnityEngine.ProBuilder.ShapeType, Unity.ProBuilder");
_faceType = Type.GetType("UnityEngine.ProBuilder.Face, Unity.ProBuilder");
_edgeType = Type.GetType("UnityEngine.ProBuilder.Edge, Unity.ProBuilder");
// MeshOperations
_extrudeElementsType = Type.GetType("UnityEngine.ProBuilder.MeshOperations.ExtrudeElements, Unity.ProBuilder");
_extrudeMethodEnum = Type.GetType("UnityEngine.ProBuilder.ExtrudeMethod, Unity.ProBuilder");
_bevelType = Type.GetType("UnityEngine.ProBuilder.MeshOperations.Bevel, Unity.ProBuilder");
_deleteElementsType = Type.GetType("UnityEngine.ProBuilder.MeshOperations.DeleteElements, Unity.ProBuilder");
_appendElementsType = Type.GetType("UnityEngine.ProBuilder.MeshOperations.AppendElements, Unity.ProBuilder");
_connectElementsType = Type.GetType("UnityEngine.ProBuilder.MeshOperations.ConnectElements, Unity.ProBuilder");
_mergeElementsType = Type.GetType("UnityEngine.ProBuilder.MeshOperations.MergeElements, Unity.ProBuilder");
_combineMeshesType = Type.GetType("UnityEngine.ProBuilder.MeshOperations.CombineMeshes, Unity.ProBuilder");
_surfaceTopologyType = Type.GetType("UnityEngine.ProBuilder.MeshOperations.SurfaceTopology, Unity.ProBuilder");
_vertexEditingType = Type.GetType("UnityEngine.ProBuilder.MeshOperations.VertexEditing, Unity.ProBuilder");
_elementSelectionType = Type.GetType("UnityEngine.ProBuilder.MeshOperations.ElementSelection, Unity.ProBuilder");
// Enums & structs
_pivotLocationType = Type.GetType("UnityEngine.ProBuilder.PivotLocation, Unity.ProBuilder");
_axisEnum = Type.GetType("UnityEngine.ProBuilder.Axis, Unity.ProBuilder");
// Editor utilities
_editorMeshUtilityType = Type.GetType("UnityEditor.ProBuilder.EditorMeshUtility, Unity.ProBuilder.Editor");
_meshImporterType = Type.GetType("UnityEngine.ProBuilder.MeshOperations.MeshImporter, Unity.ProBuilder");
_smoothingType = Type.GetType("UnityEngine.ProBuilder.Smoothing, Unity.ProBuilder");
_meshValidationType = Type.GetType("UnityEngine.ProBuilder.MeshOperations.MeshValidation, Unity.ProBuilder");
_proBuilderAvailable = true;
PatchProBuilderDefaultMaterial();
return true;
}
/// <summary>
/// Patches <c>ProBuilderDefault.mat</c> in memory to suppress unintended emission in URP projects.
/// </summary>
/// <remarks>
/// <b>Root cause:</b> The ProBuilder default material was authored in an HDRP context and ships
/// with <c>_EmissionColor = {1,1,1,1}</c> (full white) and
/// <c>m_LightmapFlags = RealtimeEmissive | BakedEmissive</c>.
/// In a URP project Unity's GI system reads these material properties <i>directly</i>,
/// bypassing the shader's own Emission block (which is correctly wired to black).
/// The result is that every fresh ProBuilder mesh is treated as a full-white emitter,
/// and any URP Bloom volume in the scene amplifies this into a visible glow artefact.
///
/// <b>Fix:</b> Zero all emission colour properties and set
/// <c>globalIlluminationFlags = EmissiveIsBlack</c> on the loaded <see cref="Material"/>
/// object. The change is in-memory only — package assets are read-only on disk — but
/// the GI system and Bloom post-process both re-query the material each frame, so the
/// patch is effective for the entire session. It is re-applied automatically on every
/// domain reload because <see cref="EnsureProBuilder"/> is called on the first MCP
/// ProBuilder command of each session.
/// </remarks>
private static void PatchProBuilderDefaultMaterial()
{
const string defaultMatPath =
"Packages/com.unity.probuilder/Content/Resources/Materials/ProBuilderDefault.mat";
var mat = AssetDatabase.LoadAssetAtPath<Material>(defaultMatPath);
if (mat == null) return;
bool changed = false;
foreach (var prop in new[] { "_EmissionColor", "_EmissionColorUI", "_EmissionColorWithMapUI" })
{
if (mat.HasProperty(prop) && mat.GetColor(prop) != Color.black)
{
mat.SetColor(prop, Color.black);
changed = true;
}
}
if (mat.globalIlluminationFlags != MaterialGlobalIlluminationFlags.EmissiveIsBlack)
{
mat.globalIlluminationFlags = MaterialGlobalIlluminationFlags.EmissiveIsBlack;
changed = true;
}
if (changed)
Debug.Log("[MCP] Patched ProBuilderDefault material: zeroed emission and set GI flags to EmissiveIsBlack.");
}
public static object HandleCommand(JObject @params)
{
if (!EnsureProBuilder())
{
return new ErrorResponse(
"ProBuilder package is not installed. Install com.unity.probuilder via Package Manager."
);
}
var p = new ToolParams(@params);
string action = p.Get("action");
if (string.IsNullOrEmpty(action))
return new ErrorResponse("Action is required");
try
{
switch (action.ToLowerInvariant())
{
case "ping":
return new SuccessResponse("ProBuilder tool is available", new { tool = "manage_probuilder" });
// Shape creation
case "create_shape": return CreateShape(@params);
case "create_poly_shape": return CreatePolyShape(@params);
// Mesh editing
case "extrude_faces": return ExtrudeFaces(@params);
case "extrude_edges": return ExtrudeEdges(@params);
case "bevel_edges": return BevelEdges(@params);
case "subdivide": return Subdivide(@params);
case "delete_faces": return DeleteFaces(@params);
case "bridge_edges": return BridgeEdges(@params);
case "connect_elements": return ConnectElements(@params);
case "detach_faces": return DetachFaces(@params);
case "flip_normals": return FlipNormals(@params);
case "merge_faces": return MergeFaces(@params);
case "combine_meshes": return CombineMeshes(@params);
case "merge_objects": return MergeObjects(@params);
case "duplicate_and_flip": return DuplicateAndFlip(@params);
case "create_polygon": return CreatePolygon(@params);
// Vertex operations
case "merge_vertices": return MergeVertices(@params);
case "weld_vertices": return WeldVertices(@params);
case "split_vertices": return SplitVertices(@params);
case "move_vertices": return MoveVertices(@params);
case "insert_vertex": return InsertVertex(@params);
case "append_vertices_to_edge": return AppendVerticesToEdge(@params);
// Selection
case "select_faces": return SelectFaces(@params);
// UV & materials
case "set_face_material": return SetFaceMaterial(@params);
case "set_face_color": return SetFaceColor(@params);
case "set_face_uvs": return SetFaceUVs(@params);
// Query
case "get_mesh_info": return GetMeshInfo(@params);
case "convert_to_probuilder": return ConvertToProBuilder(@params);
// Smoothing
case "set_smoothing": return ProBuilderSmoothing.SetSmoothing(@params);
case "auto_smooth": return ProBuilderSmoothing.AutoSmooth(@params);
// Mesh utilities
case "center_pivot": return ProBuilderMeshUtils.CenterPivot(@params);
case "freeze_transform": return ProBuilderMeshUtils.FreezeTransform(@params);
case "set_pivot": return ProBuilderMeshUtils.SetPivot(@params);
case "validate_mesh": return ProBuilderMeshUtils.ValidateMesh(@params);
case "repair_mesh": return ProBuilderMeshUtils.RepairMesh(@params);
default:
return new ErrorResponse($"Unknown action: {action}");
}
}
catch (Exception ex)
{
return new ErrorResponse(ex.Message, new { stackTrace = ex.StackTrace });
}
}
// =====================================================================
// Helpers
// =====================================================================
internal static GameObject FindTarget(JObject @params)
{
return ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString());
}
private static Component GetProBuilderMesh(GameObject go)
{
return go.GetComponent(_proBuilderMeshType);
}
internal static Component RequireProBuilderMesh(JObject @params)
{
var go = FindTarget(@params);
if (go == null)
throw new Exception("Target GameObject not found.");
var pbMesh = GetProBuilderMesh(go);
if (pbMesh == null)
throw new Exception($"GameObject '{go.name}' does not have a ProBuilderMesh component.");
return pbMesh;
}
internal static void RefreshMesh(Component pbMesh)
{
// ToMesh and Refresh have optional parameters (MeshTopology, RefreshMask) —
// Type.EmptyTypes won't find them. Use name-only lookup with default args.
var toMeshMethod = _proBuilderMeshType.GetMethod("ToMesh", Type.EmptyTypes)
?? _proBuilderMeshType.GetMethod("ToMesh", BindingFlags.Instance | BindingFlags.Public);
toMeshMethod?.Invoke(pbMesh, toMeshMethod.GetParameters().Length > 0
? new object[toMeshMethod.GetParameters().Length]
: null);
var refreshMethod = _proBuilderMeshType.GetMethod("Refresh", Type.EmptyTypes)
?? _proBuilderMeshType.GetMethod("Refresh", BindingFlags.Instance | BindingFlags.Public);
refreshMethod?.Invoke(pbMesh, refreshMethod.GetParameters().Length > 0
? new object[refreshMethod.GetParameters().Length]
: null);
if (_editorMeshUtilityType != null)
{
var optimizeMethod = _editorMeshUtilityType.GetMethod("Optimize",
BindingFlags.Static | BindingFlags.Public,
null,
new[] { _proBuilderMeshType },
null);
optimizeMethod?.Invoke(null, new object[] { pbMesh });
}
}
internal static object GetFacesArray(Component pbMesh)
{
var facesProperty = _proBuilderMeshType.GetProperty("faces");
return facesProperty?.GetValue(pbMesh);
}
internal static Array GetFacesByIndices(Component pbMesh, JToken faceIndicesToken)
{
var allFaces = GetFacesArray(pbMesh);
if (allFaces == null)
throw new Exception("Could not read faces from ProBuilderMesh.");
var facesList = (System.Collections.IList)allFaces;
if (faceIndicesToken == null)
{
// Return all faces when no indices specified
var allResult = Array.CreateInstance(_faceType, facesList.Count);
for (int i = 0; i < facesList.Count; i++)
allResult.SetValue(facesList[i], i);
return allResult;
}
var indices = faceIndicesToken.ToObject<int[]>();
var result = Array.CreateInstance(_faceType, indices.Length);
for (int i = 0; i < indices.Length; i++)
{
if (indices[i] < 0 || indices[i] >= facesList.Count)
throw new Exception($"Face index {indices[i]} out of range (0-{facesList.Count - 1}).");
result.SetValue(facesList[indices[i]], i);
}
return result;
}
internal static JObject ExtractProperties(JObject @params)
{
var propsToken = @params["properties"];
if (propsToken is JObject jObj) return jObj;
if (propsToken is JValue jVal && jVal.Type == JTokenType.String)
{
var parsed = JObject.Parse(jVal.ToString());
if (parsed != null) return parsed;
}
// Fallback: properties might be at the top level
return @params;
}
private static Vector3 ParseVector3(JToken token)
{
return VectorParsing.ParseVector3OrDefault(token);
}
internal static int GetFaceCount(Component pbMesh)
{
var faceCount = _proBuilderMeshType.GetProperty("faceCount");
return faceCount != null ? (int)faceCount.GetValue(pbMesh) : -1;
}
internal static int GetVertexCount(Component pbMesh)
{
var vertexCount = _proBuilderMeshType.GetProperty("vertexCount");
return vertexCount != null ? (int)vertexCount.GetValue(pbMesh) : -1;
}
private static object GetPivotCenter()
{
if (_pivotLocationType == null) return null;
// PivotLocation.Center = 0
return Enum.ToObject(_pivotLocationType, 0);
}
private static Component InvokeGenerator(string methodName, Type[] paramTypes, object[] args)
{
if (_shapeGeneratorType == null) return null;
var method = _shapeGeneratorType.GetMethod(methodName,
BindingFlags.Static | BindingFlags.Public,
null, paramTypes, null);
return method?.Invoke(null, args) as Component;
}
// =====================================================================
// Edge Helpers
// =====================================================================
private static int GetEdgeVertexA(object edge)
{
var f = _edgeType.GetField("a");
if (f != null) return (int)f.GetValue(edge);
var p = _edgeType.GetProperty("a");
return p != null ? (int)p.GetValue(edge) : -1;
}
private static int GetEdgeVertexB(object edge)
{
var f = _edgeType.GetField("b");
if (f != null) return (int)f.GetValue(edge);
var p = _edgeType.GetProperty("b");
return p != null ? (int)p.GetValue(edge) : -1;
}
private static object CreateEdge(int a, int b)
{
var ctor = _edgeType.GetConstructor(new[] { typeof(int), typeof(int) });
return ctor?.Invoke(new object[] { a, b });
}
/// <summary>
/// Create a typed List&lt;Face&gt; from a Face[] array for reflection calls
/// that require IEnumerable&lt;Face&gt;.
/// </summary>
private static System.Collections.IList ToTypedFaceList(Array faces)
{
var faceListType = typeof(List<>).MakeGenericType(_faceType);
var faceList = Activator.CreateInstance(faceListType) as System.Collections.IList;
foreach (var f in faces)
faceList.Add(f);
return faceList;
}
/// <summary>
/// Collect unique (deduplicated) edges from the mesh.
/// Edges shared between faces appear only once.
/// </summary>
internal static List<object> CollectUniqueEdges(Component pbMesh)
{
var allFaces = (System.Collections.IList)GetFacesArray(pbMesh);
var uniqueEdges = new List<object>();
var edgeSet = new HashSet<(int, int)>();
var edgesProp = _faceType.GetProperty("edges");
// Build shared vertex lookup so edges on different faces with different
// vertex indices but the same spatial position are correctly deduplicated.
var sharedLookup = BuildSharedVertexLookup(pbMesh);
if (allFaces != null && edgesProp != null)
{
foreach (var face in allFaces)
{
var faceEdges = edgesProp.GetValue(face) as System.Collections.IList;
if (faceEdges == null) continue;
foreach (var edge in faceEdges)
{
int a = GetEdgeVertexA(edge);
int b = GetEdgeVertexB(edge);
int sa = sharedLookup != null && sharedLookup.ContainsKey(a) ? sharedLookup[a] : a;
int sb = sharedLookup != null && sharedLookup.ContainsKey(b) ? sharedLookup[b] : b;
var key = (Math.Min(sa, sb), Math.Max(sa, sb));
if (edgeSet.Add(key))
uniqueEdges.Add(edge);
}
}
}
return uniqueEdges;
}
private static Dictionary<int, int> BuildSharedVertexLookup(Component pbMesh)
{
var sharedVerticesProp = _proBuilderMeshType.GetProperty("sharedVertices");
var sharedVertices = sharedVerticesProp?.GetValue(pbMesh) as System.Collections.IList;
if (sharedVertices == null) return null;
var lookup = new Dictionary<int, int>();
for (int groupIdx = 0; groupIdx < sharedVertices.Count; groupIdx++)
{
var group = sharedVertices[groupIdx] as System.Collections.IEnumerable;
if (group == null) continue;
foreach (object vertIdx in group)
lookup[(int)vertIdx] = groupIdx;
}
return lookup;
}
/// <summary>
/// Resolve edges from parameters. Supports:
/// - "edgeIndices" / "edge_indices": flat array of indices into unique edge list
/// - "edges": array of {a, b} vertex pair objects
/// Returns a typed Edge[] array suitable for reflection calls.
/// </summary>
private static Array ResolveEdges(Component pbMesh, JObject props, out int count)
{
var edgeIndicesToken = props["edgeIndices"] ?? props["edge_indices"];
var edgePairsToken = props["edges"];
var edgeList = new List<object>();
if (edgePairsToken != null && edgePairsToken.Type == JTokenType.Array)
{
// Edge specification by vertex pairs: [{a: 0, b: 1}, ...]
foreach (var pair in edgePairsToken)
{
int a = pair["a"]?.Value<int>() ?? 0;
int b = pair["b"]?.Value<int>() ?? 0;
edgeList.Add(CreateEdge(a, b));
}
}
else if (edgeIndicesToken != null)
{
// Edge specification by index into unique edges
var allEdges = CollectUniqueEdges(pbMesh);
var edgeIndices = edgeIndicesToken.ToObject<int[]>();
foreach (int idx in edgeIndices)
{
if (idx < 0 || idx >= allEdges.Count)
throw new Exception($"Edge index {idx} out of range (0-{allEdges.Count - 1}).");
edgeList.Add(allEdges[idx]);
}
}
else
{
throw new Exception("edgeIndices or edges parameter is required.");
}
count = edgeList.Count;
var edgeArray = Array.CreateInstance(_edgeType, edgeList.Count);
for (int i = 0; i < edgeList.Count; i++)
edgeArray.SetValue(edgeList[i], i);
return edgeArray;
}
/// <summary>
/// Create a typed List&lt;Edge&gt; from an Edge[] array for APIs that require IList&lt;Edge&gt;.
/// </summary>
private static System.Collections.IList ToTypedEdgeList(Array edgeArray)
{
var edgeListType = typeof(List<>).MakeGenericType(_edgeType);
var typedList = Activator.CreateInstance(edgeListType) as System.Collections.IList;
foreach (var e in edgeArray)
typedList.Add(e);
return typedList;
}
// =====================================================================
// Shape Creation
// =====================================================================
private static object CreateShape(JObject @params)
{
var props = ExtractProperties(@params);
string shapeTypeStr = props["shapeType"]?.ToString() ?? props["shape_type"]?.ToString();
if (string.IsNullOrEmpty(shapeTypeStr))
return new ErrorResponse("shapeType parameter is required.");
if (_shapeGeneratorType == null || _shapeTypeEnum == null)
return new ErrorResponse("ShapeGenerator or ShapeType not found in ProBuilder assembly.");
Undo.IncrementCurrentGroup();
Component pbMesh = null;
var pivot = GetPivotCenter();
// Try shape-specific generators with real dimension parameters
if (pivot != null)
pbMesh = CreateShapeViaGenerator(shapeTypeStr, props, pivot);
// Fallback: generic CreateShape(ShapeType) for unknown shapes or if generator failed
if (pbMesh == null)
pbMesh = CreateShapeGeneric(shapeTypeStr);
if (pbMesh == null)
return new ErrorResponse($"Failed to create ProBuilder shape '{shapeTypeStr}'.");
var go = pbMesh.gameObject;
Undo.RegisterCreatedObjectUndo(go, $"Create ProBuilder {shapeTypeStr}");
// Apply name
string name = props["name"]?.ToString();
if (!string.IsNullOrEmpty(name))
go.name = name;
// Apply position
var posToken = props["position"];
if (posToken != null)
go.transform.position = ParseVector3(posToken);
// Apply rotation
var rotToken = props["rotation"];
if (rotToken != null)
go.transform.eulerAngles = ParseVector3(rotToken);
RefreshMesh(pbMesh);
return new SuccessResponse($"Created ProBuilder {shapeTypeStr}: {go.name}", new
{
gameObjectName = go.name,
instanceId = go.GetInstanceIDCompat(),
shapeType = shapeTypeStr,
faceCount = GetFaceCount(pbMesh),
vertexCount = GetVertexCount(pbMesh),
});
}
private static Component CreateShapeViaGenerator(string shapeType, JObject props, object pivot)
{
float size = props["size"]?.Value<float>() ?? 0;
float width = props["width"]?.Value<float>() ?? 0;
float height = props["height"]?.Value<float>() ?? 0;
float depth = props["depth"]?.Value<float>() ?? 0;
float radius = props["radius"]?.Value<float>() ?? 0;
switch (shapeType.ToUpperInvariant())
{
case "CUBE":
{
float w = width > 0 ? width : (size > 0 ? size : 1f);
float h = height > 0 ? height : (size > 0 ? size : 1f);
float d = depth > 0 ? depth : (size > 0 ? size : 1f);
return InvokeGenerator("GenerateCube",
new[] { _pivotLocationType, typeof(Vector3) },
new object[] { pivot, new Vector3(w, h, d) });
}
case "PRISM":
{
float w = width > 0 ? width : (size > 0 ? size : 1f);
float h = height > 0 ? height : (size > 0 ? size : 1f);
float d = depth > 0 ? depth : (size > 0 ? size : 1f);
return InvokeGenerator("GeneratePrism",
new[] { _pivotLocationType, typeof(Vector3) },
new object[] { pivot, new Vector3(w, h, d) });
}
case "CYLINDER":
{
float r = radius > 0 ? radius : (size > 0 ? size / 2f : 0.5f);
float h = height > 0 ? height : (size > 0 ? size : 2f);
int axisDivisions = props["axisDivisions"]?.Value<int>()
?? props["axis_divisions"]?.Value<int>()
?? props["segments"]?.Value<int>() ?? 24;
int heightCuts = props["heightCuts"]?.Value<int>()
?? props["height_cuts"]?.Value<int>() ?? 0;
int smoothing = props["smoothing"]?.Value<int>() ?? -1;
return InvokeGenerator("GenerateCylinder",
new[] { _pivotLocationType, typeof(int), typeof(float), typeof(float), typeof(int), typeof(int) },
new object[] { pivot, axisDivisions, r, h, heightCuts, smoothing });
}
case "CONE":
{
float r = radius > 0 ? radius : (size > 0 ? size / 2f : 0.5f);
float h = height > 0 ? height : (size > 0 ? size : 1f);
int subdivAxis = props["subdivAxis"]?.Value<int>()
?? props["subdiv_axis"]?.Value<int>()
?? props["segments"]?.Value<int>() ?? 6;
return InvokeGenerator("GenerateCone",
new[] { _pivotLocationType, typeof(float), typeof(float), typeof(int) },
new object[] { pivot, r, h, subdivAxis });
}
case "SPHERE":
{
float r = radius > 0 ? radius : (size > 0 ? size / 2f : 0.5f);
int subdivisions = props["subdivisions"]?.Value<int>() ?? 2;
return InvokeGenerator("GenerateIcosahedron",
new[] { _pivotLocationType, typeof(float), typeof(int), typeof(bool), typeof(bool) },
new object[] { pivot, r, subdivisions, true, false });
}
case "TORUS":
{
int rows = props["rows"]?.Value<int>() ?? 8;
int columns = props["columns"]?.Value<int>() ?? 16;
// ProBuilder convention: innerRadius = ring radius (major), outerRadius = tube radius (minor).
// Our API uses the intuitive naming: outerRadius = ring, innerRadius = tube.
// So we swap when passing to ProBuilder's GenerateTorus.
float tubeRadius = props["innerRadius"]?.Value<float>()
?? props["inner_radius"]?.Value<float>()
?? props["tubeRadius"]?.Value<float>()
?? props["tube_radius"]?.Value<float>()
?? (radius > 0 ? radius * 0.1f : 0.1f);
float ringRadius = props["outerRadius"]?.Value<float>()
?? props["outer_radius"]?.Value<float>()
?? props["ringRadius"]?.Value<float>()
?? props["ring_radius"]?.Value<float>()
?? (radius > 0 ? radius : (size > 0 ? size / 2f : 0.5f));
bool smooth = props["smooth"]?.Value<bool>() ?? true;
float hCirc = props["horizontalCircumference"]?.Value<float>()
?? props["horizontal_circumference"]?.Value<float>() ?? 360f;
float vCirc = props["verticalCircumference"]?.Value<float>()
?? props["vertical_circumference"]?.Value<float>() ?? 360f;
return InvokeGenerator("GenerateTorus",
new[] { _pivotLocationType, typeof(int), typeof(int), typeof(float), typeof(float),
typeof(bool), typeof(float), typeof(float), typeof(bool) },
new object[] { pivot, rows, columns, ringRadius, tubeRadius, smooth, hCirc, vCirc, false });
}
case "PIPE":
{
float r = radius > 0 ? radius : (size > 0 ? size / 2f : 1f);
float h = height > 0 ? height : (size > 0 ? size : 2f);
float thickness = props["thickness"]?.Value<float>() ?? 0.2f;
int subdivAxis = props["subdivAxis"]?.Value<int>()
?? props["subdiv_axis"]?.Value<int>()
?? props["segments"]?.Value<int>() ?? 6;
int subdivHeight = props["subdivHeight"]?.Value<int>()
?? props["subdiv_height"]?.Value<int>() ?? 1;
return InvokeGenerator("GeneratePipe",
new[] { _pivotLocationType, typeof(float), typeof(float), typeof(float), typeof(int), typeof(int) },
new object[] { pivot, r, h, thickness, subdivAxis, subdivHeight });
}
case "PLANE":
{
float w = width > 0 ? width : (size > 0 ? size : 1f);
float h = height > 0 ? height : (depth > 0 ? depth : (size > 0 ? size : 1f));
int widthCuts = props["widthCuts"]?.Value<int>()
?? props["width_cuts"]?.Value<int>() ?? 0;
int heightCuts = props["heightCuts"]?.Value<int>()
?? props["height_cuts"]?.Value<int>() ?? 0;
// Axis enum: default Y-up (2)
if (_axisEnum != null)
{
int axisVal = props["axis"]?.Value<int>() ?? 2;
var axisObj = Enum.ToObject(_axisEnum, axisVal);
return InvokeGenerator("GeneratePlane",
new[] { _pivotLocationType, typeof(float), typeof(float), typeof(int), typeof(int), _axisEnum },
new object[] { pivot, w, h, widthCuts, heightCuts, axisObj });
}
return InvokeGenerator("GeneratePlane",
new[] { _pivotLocationType, typeof(float), typeof(float), typeof(int), typeof(int) },
new object[] { pivot, w, h, widthCuts, heightCuts });
}
case "STAIR":
{
float w = width > 0 ? width : (size > 0 ? size : 2f);
float h = height > 0 ? height : (size > 0 ? size : 2.5f);
float d = depth > 0 ? depth : (size > 0 ? size : 4f);
int steps = props["steps"]?.Value<int>() ?? 10;
bool buildSides = props["buildSides"]?.Value<bool>()
?? props["build_sides"]?.Value<bool>() ?? true;
return InvokeGenerator("GenerateStair",
new[] { _pivotLocationType, typeof(Vector3), typeof(int), typeof(bool) },
new object[] { pivot, new Vector3(w, h, d), steps, buildSides });
}
case "CURVEDSTAIR":
{
float stairWidth = width > 0 ? width : (size > 0 ? size : 2f);
float h = height > 0 ? height : (size > 0 ? size : 2.5f);
float innerR = props["innerRadius"]?.Value<float>()
?? props["inner_radius"]?.Value<float>()
?? (radius > 0 ? radius : 2f);
float circumference = props["circumference"]?.Value<float>() ?? 90f;
int steps = props["steps"]?.Value<int>() ?? 10;
bool buildSides = props["buildSides"]?.Value<bool>()
?? props["build_sides"]?.Value<bool>() ?? true;
return InvokeGenerator("GenerateCurvedStair",
new[] { _pivotLocationType, typeof(float), typeof(float), typeof(float), typeof(float), typeof(int), typeof(bool) },
new object[] { pivot, stairWidth, h, innerR, circumference, steps, buildSides });
}
case "ARCH":
{
float angle = props["angle"]?.Value<float>() ?? 180f;
float r = radius > 0 ? radius : (size > 0 ? size / 2f : 2f);
float w = width > 0 ? width : 0.5f;
float d = depth > 0 ? depth : 0.5f;
int radialCuts = props["radialCuts"]?.Value<int>()
?? props["radial_cuts"]?.Value<int>() ?? 6;
bool insideFaces = props["insideFaces"]?.Value<bool>()
?? props["inside_faces"]?.Value<bool>() ?? true;
bool outsideFaces = props["outsideFaces"]?.Value<bool>()
?? props["outside_faces"]?.Value<bool>() ?? true;
bool frontFaces = props["frontFaces"]?.Value<bool>()
?? props["front_faces"]?.Value<bool>() ?? true;
bool backFaces = props["backFaces"]?.Value<bool>()
?? props["back_faces"]?.Value<bool>() ?? true;
bool endCaps = props["endCaps"]?.Value<bool>()
?? props["end_caps"]?.Value<bool>() ?? true;
return InvokeGenerator("GenerateArch",
new[] { _pivotLocationType, typeof(float), typeof(float), typeof(float), typeof(float),
typeof(int), typeof(bool), typeof(bool), typeof(bool), typeof(bool), typeof(bool) },
new object[] { pivot, angle, r, w, d, radialCuts,
insideFaces, outsideFaces, frontFaces, backFaces, endCaps });
}
case "DOOR":
{
float totalWidth = width > 0 ? width : (size > 0 ? size : 4f);
float totalHeight = height > 0 ? height : (size > 0 ? size : 4f);
float ledgeHeight = props["ledgeHeight"]?.Value<float>()
?? props["ledge_height"]?.Value<float>() ?? 0.1f;
float legWidth = props["legWidth"]?.Value<float>()
?? props["leg_width"]?.Value<float>() ?? 1f;
float d = depth > 0 ? depth : (size > 0 ? size : 0.5f);
return InvokeGenerator("GenerateDoor",
new[] { _pivotLocationType, typeof(float), typeof(float), typeof(float), typeof(float), typeof(float) },
new object[] { pivot, totalWidth, totalHeight, ledgeHeight, legWidth, d });
}
default:
return null;
}
}
private static Component CreateShapeGeneric(string shapeTypeStr)
{
object shapeTypeValue;
try
{
shapeTypeValue = Enum.Parse(_shapeTypeEnum, shapeTypeStr, true);
}
catch
{
var validTypes = string.Join(", ", Enum.GetNames(_shapeTypeEnum));
throw new Exception($"Unknown shape type '{shapeTypeStr}'. Valid types: {validTypes}");
}
// Try CreateShape(ShapeType) first
var createMethod = _shapeGeneratorType.GetMethod("CreateShape",
BindingFlags.Static | BindingFlags.Public,
null,
new[] { _shapeTypeEnum },
null);
object[] invokeArgs;
if (createMethod != null)
{
invokeArgs = new[] { shapeTypeValue };
}
else if (_pivotLocationType != null)
{
createMethod = _shapeGeneratorType.GetMethod("CreateShape",
BindingFlags.Static | BindingFlags.Public,
null,
new[] { _shapeTypeEnum, _pivotLocationType },
null);
invokeArgs = new[] { shapeTypeValue, GetPivotCenter() };
}
else
{
return null;
}
return createMethod?.Invoke(null, invokeArgs) as Component;
}
private static object CreatePolyShape(JObject @params)
{
var props = ExtractProperties(@params);
var pointsToken = props["points"];
if (pointsToken == null)
return new ErrorResponse("points parameter is required.");
var points = new List<Vector3>();
foreach (var pt in pointsToken)
points.Add(ParseVector3(pt));
if (points.Count < 3)
return new ErrorResponse("At least 3 points are required for a poly shape.");
float extrudeHeight = props["extrudeHeight"]?.Value<float>() ?? props["extrude_height"]?.Value<float>() ?? 1f;
bool flipNormals = props["flipNormals"]?.Value<bool>() ?? props["flip_normals"]?.Value<bool>() ?? false;
// Create a new GameObject with ProBuilderMesh
var go = new GameObject("PolyShape");
Undo.RegisterCreatedObjectUndo(go, "Create ProBuilder PolyShape");
var pbMesh = go.AddComponent(_proBuilderMeshType);
if (_appendElementsType == null)
{
UnityEngine.Object.DestroyImmediate(go);
return new ErrorResponse("AppendElements type not found in ProBuilder assembly.");
}
var createFromPolygonMethod = _appendElementsType.GetMethod("CreateShapeFromPolygon",
BindingFlags.Static | BindingFlags.Public,
null,
new[] { _proBuilderMeshType, typeof(IList<Vector3>), typeof(float), typeof(bool) },
null);
if (createFromPolygonMethod == null)
{
UnityEngine.Object.DestroyImmediate(go);
return new ErrorResponse("CreateShapeFromPolygon method not found.");
}
createFromPolygonMethod.Invoke(null, new object[] { pbMesh, points, extrudeHeight, flipNormals });
string name = props["name"]?.ToString();
if (!string.IsNullOrEmpty(name))
go.name = name;
RefreshMesh(pbMesh);
return new SuccessResponse($"Created poly shape: {go.name}", new
{
gameObjectName = go.name,
instanceId = go.GetInstanceIDCompat(),
pointCount = points.Count,
extrudeHeight,
faceCount = GetFaceCount(pbMesh),
vertexCount = GetVertexCount(pbMesh),
});
}
// =====================================================================
// Mesh Editing
// =====================================================================
private static object ExtrudeFaces(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
var faces = GetFacesByIndices(pbMesh, props["faceIndices"] ?? props["face_indices"]);
float distance = props["distance"]?.Value<float>() ?? 0.5f;
string methodStr = props["method"]?.ToString() ?? "FaceNormal";
object extrudeMethod;
try
{
extrudeMethod = Enum.Parse(_extrudeMethodEnum, methodStr, true);
}
catch
{
return new ErrorResponse($"Unknown extrude method '{methodStr}'. Valid: FaceNormal, VertexNormal, IndividualFaces");
}
Undo.RegisterCompleteObjectUndo(pbMesh, "Extrude Faces");
var extrudeMethodInfo = _extrudeElementsType?.GetMethod("Extrude",
BindingFlags.Static | BindingFlags.Public,
null,
new[] { _proBuilderMeshType, faces.GetType(), _extrudeMethodEnum, typeof(float) },
null);
if (extrudeMethodInfo == null)
return new ErrorResponse("ExtrudeElements.Extrude method not found.");
extrudeMethodInfo.Invoke(null, new object[] { pbMesh, faces, extrudeMethod, distance });
RefreshMesh(pbMesh);
return new SuccessResponse($"Extruded {faces.Length} face(s) by {distance}", new
{
facesExtruded = faces.Length,
distance,
method = methodStr,
faceCount = GetFaceCount(pbMesh),
});
}
private static object ExtrudeEdges(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
int edgeCount;
Array edgeArray;
try
{
edgeArray = ResolveEdges(pbMesh, props, out edgeCount);
}
catch (Exception ex)
{
return new ErrorResponse(ex.Message);
}
float distance = props["distance"]?.Value<float>() ?? 0.5f;
bool asGroup = props["asGroup"]?.Value<bool>() ?? props["as_group"]?.Value<bool>() ?? true;
Undo.RegisterCompleteObjectUndo(pbMesh, "Extrude Edges");
var extrudeMethod = _extrudeElementsType?.GetMethod("Extrude",
BindingFlags.Static | BindingFlags.Public,
null,
new[] { _proBuilderMeshType, edgeArray.GetType(), typeof(float), typeof(bool), typeof(bool) },
null);
if (extrudeMethod == null)
return new ErrorResponse("ExtrudeElements.Extrude (edges) method not found.");
extrudeMethod.Invoke(null, new object[] { pbMesh, edgeArray, distance, asGroup, true });
RefreshMesh(pbMesh);
return new SuccessResponse($"Extruded {edgeCount} edge(s) by {distance}", new
{
edgesExtruded = edgeCount,
distance,
faceCount = GetFaceCount(pbMesh),
});
}
private static object BevelEdges(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
int edgeCount;
Array edgeArray;
try
{
edgeArray = ResolveEdges(pbMesh, props, out edgeCount);
}
catch (Exception ex)
{
return new ErrorResponse(ex.Message);
}
float amount = props["amount"]?.Value<float>() ?? 0.1f;
if (_bevelType == null)
return new ErrorResponse("Bevel type not found in ProBuilder assembly.");
Undo.RegisterCompleteObjectUndo(pbMesh, "Bevel Edges");
var typedList = ToTypedEdgeList(edgeArray);
var bevelMethod = _bevelType.GetMethod("BevelEdges",
BindingFlags.Static | BindingFlags.Public);
if (bevelMethod == null)
return new ErrorResponse("Bevel.BevelEdges method not found.");
bevelMethod.Invoke(null, new object[] { pbMesh, typedList, amount });
RefreshMesh(pbMesh);
return new SuccessResponse($"Beveled {edgeCount} edge(s) with amount {amount}", new
{
edgesBeveled = edgeCount,
amount,
faceCount = GetFaceCount(pbMesh),
});
}
private static object Subdivide(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
if (_connectElementsType == null)
return new ErrorResponse("ConnectElements type not found.");
Undo.RegisterCompleteObjectUndo(pbMesh, "Subdivide");
var faceIndicesToken = props["faceIndices"] ?? props["face_indices"];
// Get faces to subdivide (all faces if none specified)
var faces = GetFacesByIndices(pbMesh, faceIndicesToken);
var faceList = ToTypedFaceList(faces);
// ProBuilder uses ConnectElements.Connect(mesh, faces) for face subdivision
var connectMethod = _connectElementsType.GetMethods(BindingFlags.Static | BindingFlags.Public)
.FirstOrDefault(m => m.Name == "Connect" && m.GetParameters().Length == 2
&& m.GetParameters()[1].ParameterType.IsAssignableFrom(faceList.GetType()));
if (connectMethod == null)
return new ErrorResponse("ConnectElements.Connect (faces) method not found.");
connectMethod.Invoke(null, new object[] { pbMesh, faceList });
RefreshMesh(pbMesh);
return new SuccessResponse("Subdivided mesh", new
{
faceCount = GetFaceCount(pbMesh),
vertexCount = GetVertexCount(pbMesh),
});
}
private static object DeleteFaces(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
var faceIndicesToken = props["faceIndices"] ?? props["face_indices"];
if (faceIndicesToken == null)
return new ErrorResponse("faceIndices parameter is required.");
if (_deleteElementsType == null)
return new ErrorResponse("DeleteElements type not found.");
var faceIndices = faceIndicesToken.ToObject<int[]>();
Undo.RegisterCompleteObjectUndo(pbMesh, "Delete Faces");
// Prefer DeleteFaces(ProBuilderMesh, IList<int>) overload
var deleteMethod = _deleteElementsType.GetMethod("DeleteFaces",
BindingFlags.Static | BindingFlags.Public,
null,
new[] { _proBuilderMeshType, typeof(IList<int>) },
null);
if (deleteMethod != null)
{
deleteMethod.Invoke(null, new object[] { pbMesh, faceIndices.ToList() });
}
else
{
// Try int[] overload
deleteMethod = _deleteElementsType.GetMethod("DeleteFaces",
BindingFlags.Static | BindingFlags.Public,
null,
new[] { _proBuilderMeshType, typeof(int[]) },
null);
if (deleteMethod == null)
{
// Try IEnumerable<Face> overload
var faces = GetFacesByIndices(pbMesh, faceIndicesToken);
deleteMethod = _deleteElementsType.GetMethod("DeleteFaces",
BindingFlags.Static | BindingFlags.Public,
null,
new[] { _proBuilderMeshType, faces.GetType() },
null);
if (deleteMethod == null)
return new ErrorResponse("DeleteElements.DeleteFaces method not found.");
deleteMethod.Invoke(null, new object[] { pbMesh, faces });
}
else
{
deleteMethod.Invoke(null, new object[] { pbMesh, faceIndices });
}
}
RefreshMesh(pbMesh);
return new SuccessResponse($"Deleted {faceIndices.Length} face(s)", new
{
facesDeleted = faceIndices.Length,
faceCount = GetFaceCount(pbMesh),
});
}
private static object BridgeEdges(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
if (_appendElementsType == null)
return new ErrorResponse("AppendElements type not found.");
var edgeAToken = props["edgeA"] ?? props["edge_a"];
var edgeBToken = props["edgeB"] ?? props["edge_b"];
if (edgeAToken == null || edgeBToken == null)
return new ErrorResponse("edgeA and edgeB parameters are required (as {a, b} vertex index pairs).");
int aA = edgeAToken["a"]?.Value<int>() ?? 0;
int aB = edgeAToken["b"]?.Value<int>() ?? 0;
int bA = edgeBToken["a"]?.Value<int>() ?? 0;
int bB = edgeBToken["b"]?.Value<int>() ?? 0;
var edgeA = CreateEdge(aA, aB);
var edgeB = CreateEdge(bA, bB);
bool allowNonManifold = props["allowNonManifold"]?.Value<bool>()
?? props["allow_non_manifold"]?.Value<bool>()
?? props["allowNonManifoldGeometry"]?.Value<bool>()
?? props["allow_non_manifold_geometry"]?.Value<bool>()
?? false;
Undo.RegisterCompleteObjectUndo(pbMesh, "Bridge Edges");
// Try overload with allowNonManifoldGeometry parameter first
var bridgeMethod = _appendElementsType.GetMethod("Bridge",
BindingFlags.Static | BindingFlags.Public,
null,
new[] { _proBuilderMeshType, _edgeType, _edgeType, typeof(bool) },
null);
object result;
if (bridgeMethod != null)
{
result = bridgeMethod.Invoke(null, new object[] { pbMesh, edgeA, edgeB, allowNonManifold });
}
else
{
// Fallback without allowNonManifold
bridgeMethod = _appendElementsType.GetMethod("Bridge",
BindingFlags.Static | BindingFlags.Public,
null,
new[] { _proBuilderMeshType, _edgeType, _edgeType },
null);
if (bridgeMethod == null)
return new ErrorResponse("AppendElements.Bridge method not found.");
result = bridgeMethod.Invoke(null, new object[] { pbMesh, edgeA, edgeB });
}
RefreshMesh(pbMesh);
return new SuccessResponse("Bridged edges", new
{
bridgeCreated = result != null,
faceCount = GetFaceCount(pbMesh),
});
}
private static object ConnectElements(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
if (_connectElementsType == null)
return new ErrorResponse("ConnectElements type not found.");
Undo.RegisterCompleteObjectUndo(pbMesh, "Connect Elements");
var faceIndicesToken = props["faceIndices"] ?? props["face_indices"];
var edgeIndicesToken = props["edgeIndices"] ?? props["edge_indices"];
var edgePairsToken = props["edges"];
if (faceIndicesToken != null)
{
var faces = GetFacesByIndices(pbMesh, faceIndicesToken);
var faceList = ToTypedFaceList(faces);
// Try Connect(ProBuilderMesh, IEnumerable<Face>)
var connectMethod = _connectElementsType.GetMethods(BindingFlags.Static | BindingFlags.Public)
.FirstOrDefault(m => m.Name == "Connect" && m.GetParameters().Length == 2
&& m.GetParameters()[1].ParameterType.IsAssignableFrom(faceList.GetType()));
if (connectMethod == null)
return new ErrorResponse("ConnectElements.Connect (faces) method not found.");
connectMethod.Invoke(null, new object[] { pbMesh, faceList });
}
else if (edgeIndicesToken != null || edgePairsToken != null)
{
int edgeCount;
Array edgeArray;
try
{
edgeArray = ResolveEdges(pbMesh, props, out edgeCount);
}
catch (Exception ex)
{
return new ErrorResponse(ex.Message);
}
var typedList = ToTypedEdgeList(edgeArray);
var edgeListType = typedList.GetType();
var connectMethod = _connectElementsType.GetMethods(BindingFlags.Static | BindingFlags.Public)
.FirstOrDefault(m => m.Name == "Connect" && m.GetParameters().Length == 2
&& m.GetParameters()[1].ParameterType.IsAssignableFrom(edgeListType));
if (connectMethod == null)
return new ErrorResponse("ConnectElements.Connect (edges) method not found.");
connectMethod.Invoke(null, new object[] { pbMesh, typedList });
}
else
{
return new ErrorResponse("Either faceIndices or edgeIndices/edges parameter is required.");
}
RefreshMesh(pbMesh);
return new SuccessResponse("Connected elements", new
{
faceCount = GetFaceCount(pbMesh),
});
}
private static object DetachFaces(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
var faces = GetFacesByIndices(pbMesh, props["faceIndices"] ?? props["face_indices"]);
if (_extrudeElementsType == null)
return new ErrorResponse("ExtrudeElements type not found.");
bool deleteSource = props["deleteSourceFaces"]?.Value<bool>()
?? props["delete_source_faces"]?.Value<bool>()
?? props["deleteSource"]?.Value<bool>()
?? props["delete_source"]?.Value<bool>()
?? false;
Undo.RegisterCompleteObjectUndo(pbMesh, "Detach Faces");
var faceList = ToTypedFaceList(faces);
// Try overload: DetachFaces(ProBuilderMesh, IEnumerable<Face>, bool)
var detachMethod = _extrudeElementsType.GetMethods(BindingFlags.Static | BindingFlags.Public)
.FirstOrDefault(m => m.Name == "DetachFaces" && m.GetParameters().Length == 3
&& m.GetParameters()[1].ParameterType.IsAssignableFrom(faceList.GetType())
&& m.GetParameters()[2].ParameterType == typeof(bool));
if (detachMethod != null)
{
detachMethod.Invoke(null, new object[] { pbMesh, faceList, deleteSource });
}
else
{
// Fallback: DetachFaces(ProBuilderMesh, IEnumerable<Face>)
detachMethod = _extrudeElementsType.GetMethods(BindingFlags.Static | BindingFlags.Public)
.FirstOrDefault(m => m.Name == "DetachFaces" && m.GetParameters().Length == 2
&& m.GetParameters()[1].ParameterType.IsAssignableFrom(faceList.GetType()));
if (detachMethod == null)
return new ErrorResponse("ExtrudeElements.DetachFaces method not found.");
detachMethod.Invoke(null, new object[] { pbMesh, faceList });
}
RefreshMesh(pbMesh);
return new SuccessResponse($"Detached {faces.Length} face(s)", new
{
facesDetached = faces.Length,
deleteSourceFaces = deleteSource,
faceCount = GetFaceCount(pbMesh),
});
}
private static object FlipNormals(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
var faces = GetFacesByIndices(pbMesh, props["faceIndices"] ?? props["face_indices"]);
Undo.RegisterCompleteObjectUndo(pbMesh, "Flip Normals");
var reverseMethod = _faceType.GetMethod("Reverse");
if (reverseMethod == null)
return new ErrorResponse("Face.Reverse method not found.");
foreach (var face in faces)
reverseMethod.Invoke(face, null);
RefreshMesh(pbMesh);
return new SuccessResponse($"Flipped normals on {faces.Length} face(s)", new
{
facesFlipped = faces.Length,
});
}
private static object MergeFaces(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
var faces = GetFacesByIndices(pbMesh, props["faceIndices"] ?? props["face_indices"]);
if (_mergeElementsType == null)
return new ErrorResponse("MergeElements type not found.");
Undo.RegisterCompleteObjectUndo(pbMesh, "Merge Faces");
var faceList = ToTypedFaceList(faces);
var mergeMethod = _mergeElementsType.GetMethods(BindingFlags.Static | BindingFlags.Public)
.FirstOrDefault(m => m.Name == "Merge" && m.GetParameters().Length == 2
&& m.GetParameters()[1].ParameterType.IsAssignableFrom(faceList.GetType()));
if (mergeMethod == null)
return new ErrorResponse("MergeElements.Merge method not found.");
mergeMethod.Invoke(null, new object[] { pbMesh, faceList });
RefreshMesh(pbMesh);
return new SuccessResponse($"Merged {faces.Length} face(s)", new
{
facesMerged = faces.Length,
faceCount = GetFaceCount(pbMesh),
});
}
private static object CombineMeshes(JObject @params)
{
var props = ExtractProperties(@params);
var targetsToken = props["targets"];
if (targetsToken == null)
return new ErrorResponse("targets parameter is required (list of GameObject names/paths/ids).");
if (_combineMeshesType == null)
return new ErrorResponse("CombineMeshes type not found.");
var targets = targetsToken.ToObject<string[]>();
var pbMeshes = new List<Component>();
foreach (var targetStr in targets)
{
var go = ObjectResolver.ResolveGameObject(targetStr, null);
if (go == null)
return new ErrorResponse($"GameObject not found: {targetStr}");
var pbMesh = GetProBuilderMesh(go);
if (pbMesh == null)
return new ErrorResponse($"GameObject '{go.name}' does not have a ProBuilderMesh component.");
pbMeshes.Add(pbMesh);
}
if (pbMeshes.Count < 2)
return new ErrorResponse("At least 2 ProBuilder meshes are required for combining.");
Undo.RegisterCompleteObjectUndo(pbMeshes[0], "Combine Meshes");
var listType = typeof(List<>).MakeGenericType(_proBuilderMeshType);
var typedList = Activator.CreateInstance(listType) as System.Collections.IList;
foreach (var m in pbMeshes)
typedList.Add(m);
var combineMethod = _combineMeshesType.GetMethod("Combine",
BindingFlags.Static | BindingFlags.Public);
if (combineMethod == null)
return new ErrorResponse("CombineMeshes.Combine method not found.");
combineMethod.Invoke(null, new object[] { typedList, pbMeshes[0] });
RefreshMesh(pbMeshes[0]);
return new SuccessResponse($"Combined {pbMeshes.Count} meshes", new
{
meshesCombined = pbMeshes.Count,
targetName = pbMeshes[0].gameObject.name,
faceCount = GetFaceCount(pbMeshes[0]),
});
}
private static Component ConvertToProBuilderInternal(GameObject go)
{
var existingPB = GetProBuilderMesh(go);
if (existingPB != null)
return existingPB;
var meshFilter = go.GetComponent<MeshFilter>();
if (meshFilter == null || meshFilter.sharedMesh == null)
return null;
if (_meshImporterType == null)
return null;
var pbMesh = go.AddComponent(_proBuilderMeshType);
var importerCtor = _meshImporterType.GetConstructor(new[] { _proBuilderMeshType });
if (importerCtor == null)
return null;
var importer = importerCtor.Invoke(new object[] { pbMesh });
var importM = _meshImporterType.GetMethod("Import",
BindingFlags.Instance | BindingFlags.Public,
null,
new[] { typeof(Mesh) },
null);
if (importM == null)
importM = _meshImporterType.GetMethod("Import",
BindingFlags.Instance | BindingFlags.Public);
if (importM != null)
importM.Invoke(importer, new object[] { meshFilter.sharedMesh });
RefreshMesh(pbMesh);
return pbMesh;
}
private static object MergeObjects(JObject @params)
{
var props = ExtractProperties(@params);
var targetsToken = props["targets"];
if (targetsToken == null)
return new ErrorResponse("targets parameter is required (list of GameObject names/paths/ids).");
if (_combineMeshesType == null)
return new ErrorResponse("CombineMeshes type not found. Ensure ProBuilder is installed.");
var targets = targetsToken.ToObject<string[]>();
if (targets.Length < 2)
return new ErrorResponse("At least 2 targets are required for merging.");
var pbMeshes = new List<Component>();
var nonPbObjects = new List<GameObject>();
foreach (var targetStr in targets)
{
var go = ObjectResolver.ResolveGameObject(targetStr, null);
if (go == null)
return new ErrorResponse($"GameObject not found: {targetStr}");
var pbMesh = GetProBuilderMesh(go);
if (pbMesh != null)
pbMeshes.Add(pbMesh);
else
nonPbObjects.Add(go);
}
foreach (var go in nonPbObjects)
{
var converted = ConvertToProBuilderInternal(go);
if (converted == null)
return new ErrorResponse($"Failed to convert '{go.name}' to ProBuilder mesh.");
pbMeshes.Add(converted);
}
if (pbMeshes.Count < 2)
return new ErrorResponse("Need at least 2 meshes after conversion.");
Undo.RegisterCompleteObjectUndo(pbMeshes[0], "Merge Objects");
var listType = typeof(List<>).MakeGenericType(_proBuilderMeshType);
var typedList = Activator.CreateInstance(listType) as System.Collections.IList;
foreach (var m in pbMeshes)
typedList.Add(m);
var combineMethod = _combineMeshesType.GetMethod("Combine",
BindingFlags.Static | BindingFlags.Public);
if (combineMethod == null)
return new ErrorResponse("CombineMeshes.Combine method not found.");
combineMethod.Invoke(null, new object[] { typedList, pbMeshes[0] });
RefreshMesh(pbMeshes[0]);
string resultName = props["name"]?.ToString();
if (!string.IsNullOrEmpty(resultName))
pbMeshes[0].gameObject.name = resultName;
return new SuccessResponse($"Merged {targets.Length} objects into '{pbMeshes[0].gameObject.name}'", new
{
mergedCount = targets.Length,
convertedCount = nonPbObjects.Count,
targetName = pbMeshes[0].gameObject.name,
faceCount = GetFaceCount(pbMeshes[0]),
vertexCount = GetVertexCount(pbMeshes[0]),
});
}
private static object DuplicateAndFlip(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
var faces = GetFacesByIndices(pbMesh, props["faceIndices"] ?? props["face_indices"]);
if (_appendElementsType == null)
return new ErrorResponse("AppendElements type not found.");
Undo.RegisterCompleteObjectUndo(pbMesh, "Duplicate and Flip");
// DuplicateAndFlip(ProBuilderMesh, Face[])
var faceArrayType = Array.CreateInstance(_faceType, 0).GetType();
var dupMethod = _appendElementsType.GetMethod("DuplicateAndFlip",
BindingFlags.Static | BindingFlags.Public,
null,
new[] { _proBuilderMeshType, faceArrayType },
null);
if (dupMethod == null)
return new ErrorResponse("AppendElements.DuplicateAndFlip method not found.");
dupMethod.Invoke(null, new object[] { pbMesh, faces });
RefreshMesh(pbMesh);
return new SuccessResponse($"Duplicated and flipped {faces.Length} face(s)", new
{
facesDuplicated = faces.Length,
faceCount = GetFaceCount(pbMesh),
});
}
private static object CreatePolygon(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
var vertexIndicesToken = props["vertexIndices"] ?? props["vertex_indices"];
if (vertexIndicesToken == null)
return new ErrorResponse("vertexIndices parameter is required.");
if (_appendElementsType == null)
return new ErrorResponse("AppendElements type not found.");
var vertexIndices = vertexIndicesToken.ToObject<int[]>();
bool unordered = props["unordered"]?.Value<bool>() ?? true;
Undo.RegisterCompleteObjectUndo(pbMesh, "Create Polygon");
// CreatePolygon(ProBuilderMesh, IList<int>, bool)
var createPolyMethod = _appendElementsType.GetMethod("CreatePolygon",
BindingFlags.Static | BindingFlags.Public,
null,
new[] { _proBuilderMeshType, typeof(IList<int>), typeof(bool) },
null);
if (createPolyMethod == null)
return new ErrorResponse("AppendElements.CreatePolygon method not found.");
var result = createPolyMethod.Invoke(null, new object[] { pbMesh, vertexIndices.ToList(), unordered });
RefreshMesh(pbMesh);
return new SuccessResponse($"Created polygon from {vertexIndices.Length} vertices", new
{
vertexCount = vertexIndices.Length,
unordered,
faceCreated = result != null,
faceCount = GetFaceCount(pbMesh),
});
}
// =====================================================================
// Vertex Operations
// =====================================================================
private static object MergeVertices(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
var vertexIndicesToken = props["vertexIndices"] ?? props["vertex_indices"];
if (vertexIndicesToken == null)
return new ErrorResponse("vertexIndices parameter is required.");
var vertexIndices = vertexIndicesToken.ToObject<int[]>();
bool collapseToFirst = props["collapseToFirst"]?.Value<bool>()
?? props["collapse_to_first"]?.Value<bool>()
?? false;
if (_vertexEditingType == null)
return new ErrorResponse("VertexEditing type not found.");
Undo.RegisterCompleteObjectUndo(pbMesh, "Merge Vertices");
// MergeVertices(ProBuilderMesh mesh, int[] indexes, bool collapseToFirst = false)
var mergeMethod = _vertexEditingType.GetMethod("MergeVertices",
BindingFlags.Static | BindingFlags.Public);
if (mergeMethod == null)
return new ErrorResponse("VertexEditing.MergeVertices method not found.");
var result = mergeMethod.Invoke(null, new object[] { pbMesh, vertexIndices, collapseToFirst });
RefreshMesh(pbMesh);
return new SuccessResponse($"Merged {vertexIndices.Length} vertices", new
{
verticesMerged = vertexIndices.Length,
collapseToFirst,
resultIndex = result is int idx ? idx : -1,
vertexCount = GetVertexCount(pbMesh),
});
}
private static object WeldVertices(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
var vertexIndicesToken = props["vertexIndices"] ?? props["vertex_indices"];
if (vertexIndicesToken == null)
return new ErrorResponse("vertexIndices parameter is required.");
var vertexIndices = vertexIndicesToken.ToObject<int[]>();
float neighborRadius = props["radius"]?.Value<float>()
?? props["neighborRadius"]?.Value<float>()
?? props["neighbor_radius"]?.Value<float>()
?? 0.01f;
if (_vertexEditingType == null)
return new ErrorResponse("VertexEditing type not found.");
Undo.RegisterCompleteObjectUndo(pbMesh, "Weld Vertices");
// WeldVertices(ProBuilderMesh mesh, IEnumerable<int> indexes, float neighborRadius)
var weldMethod = _vertexEditingType.GetMethod("WeldVertices",
BindingFlags.Static | BindingFlags.Public);
if (weldMethod == null)
return new ErrorResponse("VertexEditing.WeldVertices method not found.");
var result = weldMethod.Invoke(null, new object[] { pbMesh, vertexIndices.ToList(), neighborRadius });
RefreshMesh(pbMesh);
int[] newIndices = result as int[] ?? Array.Empty<int>();
return new SuccessResponse($"Welded vertices within radius {neighborRadius}", new
{
inputCount = vertexIndices.Length,
resultCount = newIndices.Length,
radius = neighborRadius,
vertexCount = GetVertexCount(pbMesh),
});
}
private static object SplitVertices(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
var vertexIndicesToken = props["vertexIndices"] ?? props["vertex_indices"];
if (vertexIndicesToken == null)
return new ErrorResponse("vertexIndices parameter is required.");
var vertexIndices = vertexIndicesToken.ToObject<int[]>();
if (_vertexEditingType == null)
return new ErrorResponse("VertexEditing type not found.");
Undo.RegisterCompleteObjectUndo(pbMesh, "Split Vertices");
// SplitVertices(ProBuilderMesh mesh, IEnumerable<int> vertices)
var splitMethod = _vertexEditingType.GetMethod("SplitVertices",
BindingFlags.Static | BindingFlags.Public,
null,
new[] { _proBuilderMeshType, typeof(IEnumerable<int>) },
null);
if (splitMethod == null)
{
// Fallback: try any 2-param overload
splitMethod = _vertexEditingType.GetMethods(BindingFlags.Static | BindingFlags.Public)
.FirstOrDefault(m => m.Name == "SplitVertices" && m.GetParameters().Length == 2
&& m.GetParameters()[0].ParameterType == _proBuilderMeshType);
}
if (splitMethod == null)
return new ErrorResponse("VertexEditing.SplitVertices method not found.");
splitMethod.Invoke(null, new object[] { pbMesh, vertexIndices.ToList() });
RefreshMesh(pbMesh);
return new SuccessResponse($"Split {vertexIndices.Length} vertices", new
{
verticesSplit = vertexIndices.Length,
vertexCount = GetVertexCount(pbMesh),
});
}
private static object MoveVertices(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
var vertexIndicesToken = props["vertexIndices"] ?? props["vertex_indices"];
if (vertexIndicesToken == null)
return new ErrorResponse("vertexIndices parameter is required.");
var offsetToken = props["offset"];
if (offsetToken == null)
return new ErrorResponse("offset parameter is required ([x,y,z]).");
var vertexIndices = vertexIndicesToken.ToObject<int[]>();
var offset = ParseVector3(offsetToken);
Undo.RegisterCompleteObjectUndo(pbMesh, "Move Vertices");
// Get positions via property and modify directly
var positionsProperty = _proBuilderMeshType.GetProperty("positions");
if (positionsProperty == null)
return new ErrorResponse("Could not access positions property.");
var positions = positionsProperty.GetValue(pbMesh) as IList<Vector3>;
if (positions == null)
return new ErrorResponse("Could not read positions.");
var posList = new List<Vector3>(positions);
foreach (int idx in vertexIndices)
{
if (idx < 0 || idx >= posList.Count)
return new ErrorResponse($"Vertex index {idx} out of range (0-{posList.Count - 1}).");
posList[idx] += offset;
}
// Set positions back via property setter
if (positionsProperty.CanWrite)
{
positionsProperty.SetValue(pbMesh, posList);
}
else
{
// Try SetPositions method
var setPositionsMethod = _proBuilderMeshType.GetMethod("SetPositions",
BindingFlags.Instance | BindingFlags.Public);
if (setPositionsMethod != null)
{
setPositionsMethod.Invoke(pbMesh, new object[] { posList.ToArray() });
}
else
{
// Try RebuildWithPositionsAndFaces
var rebuildMethod = _proBuilderMeshType.GetMethod("RebuildWithPositionsAndFaces",
BindingFlags.Instance | BindingFlags.Public);
if (rebuildMethod != null)
{
var allFaces = GetFacesArray(pbMesh);
rebuildMethod.Invoke(pbMesh, new object[] { posList, allFaces });
}
else
{
return new ErrorResponse("Cannot set vertex positions on ProBuilderMesh.");
}
}
}
RefreshMesh(pbMesh);
return new SuccessResponse($"Moved {vertexIndices.Length} vertices by ({offset.x}, {offset.y}, {offset.z})", new
{
verticesMoved = vertexIndices.Length,
offset = new[] { offset.x, offset.y, offset.z },
});
}
private static object InsertVertex(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
if (_appendElementsType == null)
return new ErrorResponse("AppendElements type not found.");
var pointToken = props["point"] ?? props["position"];
if (pointToken == null)
return new ErrorResponse("point parameter is required ([x,y,z] in local space).");
var point = ParseVector3(pointToken);
Undo.RegisterCompleteObjectUndo(pbMesh, "Insert Vertex");
var edgeToken = props["edge"];
if (edgeToken != null)
{
// InsertVertexOnEdge(ProBuilderMesh mesh, Edge edge, Vector3 point)
int a = edgeToken["a"]?.Value<int>() ?? 0;
int b = edgeToken["b"]?.Value<int>() ?? 0;
var edge = CreateEdge(a, b);
var insertMethod = _appendElementsType.GetMethod("InsertVertexOnEdge",
BindingFlags.Static | BindingFlags.Public,
null,
new[] { _proBuilderMeshType, _edgeType, typeof(Vector3) },
null);
if (insertMethod == null)
return new ErrorResponse("AppendElements.InsertVertexOnEdge method not found.");
insertMethod.Invoke(null, new object[] { pbMesh, edge, point });
}
else
{
var faceIndexToken = props["faceIndex"] ?? props["face_index"];
if (faceIndexToken == null)
return new ErrorResponse("Either edge ({a,b}) or faceIndex parameter is required.");
int faceIndex = faceIndexToken.Value<int>();
var allFaces = (System.Collections.IList)GetFacesArray(pbMesh);
if (faceIndex < 0 || faceIndex >= allFaces.Count)
return new ErrorResponse($"Face index {faceIndex} out of range (0-{allFaces.Count - 1}).");
var face = allFaces[faceIndex];
// InsertVertexInFace(ProBuilderMesh mesh, Face face, Vector3 point)
var insertMethod = _appendElementsType.GetMethod("InsertVertexInFace",
BindingFlags.Static | BindingFlags.Public,
null,
new[] { _proBuilderMeshType, _faceType, typeof(Vector3) },
null);
if (insertMethod == null)
return new ErrorResponse("AppendElements.InsertVertexInFace method not found.");
insertMethod.Invoke(null, new object[] { pbMesh, face, point });
}
RefreshMesh(pbMesh);
return new SuccessResponse("Inserted vertex", new
{
point = new[] { point.x, point.y, point.z },
vertexCount = GetVertexCount(pbMesh),
faceCount = GetFaceCount(pbMesh),
});
}
private static object AppendVerticesToEdge(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
if (_appendElementsType == null)
return new ErrorResponse("AppendElements type not found.");
int count = props["count"]?.Value<int>() ?? 1;
Undo.RegisterCompleteObjectUndo(pbMesh, "Append Vertices to Edge");
int edgeCount;
Array edgeArray;
try
{
edgeArray = ResolveEdges(pbMesh, props, out edgeCount);
}
catch (Exception ex)
{
return new ErrorResponse(ex.Message);
}
var typedList = ToTypedEdgeList(edgeArray);
var edgeListType = typedList.GetType();
// AppendVerticesToEdge(ProBuilderMesh mesh, IList<Edge> edges, int count)
var appendMethod = _appendElementsType.GetMethod("AppendVerticesToEdge",
BindingFlags.Static | BindingFlags.Public,
null,
new[] { _proBuilderMeshType, edgeListType, typeof(int) },
null);
if (appendMethod == null)
{
// Try IList<Edge> interface match
appendMethod = _appendElementsType.GetMethods(BindingFlags.Static | BindingFlags.Public)
.FirstOrDefault(m => m.Name == "AppendVerticesToEdge" && m.GetParameters().Length == 3
&& m.GetParameters()[2].ParameterType == typeof(int));
}
if (appendMethod == null)
return new ErrorResponse("AppendElements.AppendVerticesToEdge method not found.");
appendMethod.Invoke(null, new object[] { pbMesh, typedList, count });
RefreshMesh(pbMesh);
return new SuccessResponse($"Inserted {count} point(s) on {edgeCount} edge(s)", new
{
edgesModified = edgeCount,
pointsPerEdge = count,
vertexCount = GetVertexCount(pbMesh),
faceCount = GetFaceCount(pbMesh),
});
}
// =====================================================================
// Selection
// =====================================================================
private static object SelectFaces(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
var allFaces = GetFacesArray(pbMesh);
var facesList = (System.Collections.IList)allFaces;
var selectedSet = new HashSet<int>();
var selectedIndices = new List<int>();
// Selection by direction
var directionStr = props["direction"]?.ToString();
if (!string.IsNullOrEmpty(directionStr))
{
float tolerance = props["tolerance"]?.Value<float>() ?? 0.7f;
Vector3 targetDir;
switch (directionStr.ToLowerInvariant())
{
case "up": case "top": targetDir = Vector3.up; break;
case "down": case "bottom": targetDir = Vector3.down; break;
case "forward": case "front": targetDir = Vector3.forward; break;
case "back": case "backward": targetDir = Vector3.back; break;
case "left": targetDir = Vector3.left; break;
case "right": targetDir = Vector3.right; break;
default:
return new ErrorResponse($"Unknown direction '{directionStr}'. Valid: up/down/forward/back/left/right");
}
for (int i = 0; i < facesList.Count; i++)
{
var normal = ComputeFaceNormal(pbMesh, facesList[i]);
if (Vector3.Dot(normal, targetDir) > tolerance)
{
selectedSet.Add(i);
selectedIndices.Add(i);
}
}
}
// Grow selection from existing faces
var growFromToken = props["growFrom"] ?? props["grow_from"];
var growAngle = props["growAngle"]?.Value<float>() ?? props["grow_angle"]?.Value<float>() ?? -1f;
if (growFromToken != null && _elementSelectionType != null)
{
var seedFaces = GetFacesByIndices(pbMesh, growFromToken);
var seedList = ToTypedFaceList(seedFaces);
var growMethod = _elementSelectionType.GetMethods(BindingFlags.Static | BindingFlags.Public)
.FirstOrDefault(m => m.Name == "GrowSelection" && m.GetParameters().Length == 3);
if (growMethod != null)
{
var result = growMethod.Invoke(null, new object[] { pbMesh, seedList, growAngle });
if (result is System.Collections.IEnumerable resultFaces)
{
foreach (var face in resultFaces)
{
int idx = IndexOfFace(facesList, face);
if (idx >= 0 && selectedSet.Add(idx))
selectedIndices.Add(idx);
}
}
}
}
// Flood selection from existing faces
var floodFromToken = props["floodFrom"] ?? props["flood_from"];
var floodAngle = props["floodAngle"]?.Value<float>() ?? props["flood_angle"]?.Value<float>() ?? 15f;
if (floodFromToken != null && _elementSelectionType != null)
{
var seedFaces = GetFacesByIndices(pbMesh, floodFromToken);
var seedList = ToTypedFaceList(seedFaces);
var floodMethod = _elementSelectionType.GetMethods(BindingFlags.Static | BindingFlags.Public)
.FirstOrDefault(m => m.Name == "FloodSelection" && m.GetParameters().Length == 3);
if (floodMethod != null)
{
var result = floodMethod.Invoke(null, new object[] { pbMesh, seedList, floodAngle });
if (result is System.Collections.IEnumerable resultFaces)
{
foreach (var face in resultFaces)
{
int idx = IndexOfFace(facesList, face);
if (idx >= 0 && selectedSet.Add(idx))
selectedIndices.Add(idx);
}
}
}
}
// Loop/ring selection
var loopFromToken = props["loopFrom"] ?? props["loop_from"];
bool ring = props["ring"]?.Value<bool>() ?? false;
if (loopFromToken != null && _elementSelectionType != null)
{
var seedFaces = GetFacesByIndices(pbMesh, loopFromToken);
var faceArrayType = Array.CreateInstance(_faceType, 0).GetType();
var loopMethod = _elementSelectionType.GetMethods(BindingFlags.Static | BindingFlags.Public)
.FirstOrDefault(m => m.Name == "GetFaceLoop" && m.GetParameters().Length >= 2);
if (loopMethod != null)
{
object result;
if (loopMethod.GetParameters().Length == 3)
result = loopMethod.Invoke(null, new object[] { pbMesh, seedFaces, ring });
else
result = loopMethod.Invoke(null, new object[] { pbMesh, seedFaces });
if (result is System.Collections.IEnumerable resultFaces)
{
foreach (var face in resultFaces)
{
int idx = IndexOfFace(facesList, face);
if (idx >= 0 && selectedSet.Add(idx))
selectedIndices.Add(idx);
}
}
}
}
selectedIndices.Sort();
return new SuccessResponse($"Selected {selectedIndices.Count} face(s)", new
{
faceIndices = selectedIndices,
count = selectedIndices.Count,
totalFaces = facesList.Count,
});
}
private static int IndexOfFace(System.Collections.IList facesList, object face)
{
for (int i = 0; i < facesList.Count; i++)
{
if (ReferenceEquals(facesList[i], face))
return i;
}
return -1;
}
// =====================================================================
// UV & Materials
// =====================================================================
private static object SetFaceMaterial(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
var faces = GetFacesByIndices(pbMesh, props["faceIndices"] ?? props["face_indices"]);
string materialPath = props["materialPath"]?.ToString() ?? props["material_path"]?.ToString();
if (string.IsNullOrEmpty(materialPath))
return new ErrorResponse("materialPath parameter is required.");
var material = AssetDatabase.LoadAssetAtPath<Material>(materialPath);
if (material == null)
return new ErrorResponse($"Material not found at path: {materialPath}");
Undo.RegisterCompleteObjectUndo(pbMesh, "Set Face Material");
var setMaterialMethod = _proBuilderMeshType.GetMethod("SetMaterial",
BindingFlags.Instance | BindingFlags.Public);
if (setMaterialMethod == null)
return new ErrorResponse("SetMaterial method not found on ProBuilderMesh.");
setMaterialMethod.Invoke(pbMesh, new object[] { faces, material });
// Before RefreshMesh, compact renderer materials to only those referenced by faces.
// ProBuilder's SetMaterial adds new materials to the renderer array but doesn't
// remove unused ones, causing "more materials than submeshes" warnings.
var meshRenderer = pbMesh.gameObject.GetComponent<MeshRenderer>();
if (meshRenderer != null)
{
var allFacesList = (System.Collections.IList)GetFacesArray(pbMesh);
var submeshIndexProp = _faceType.GetProperty("submeshIndex");
var currentMats = meshRenderer.sharedMaterials;
var usedIndices = new SortedSet<int>();
foreach (var f in allFacesList)
usedIndices.Add((int)submeshIndexProp.GetValue(f));
if (usedIndices.Count < currentMats.Length)
{
var remap = new Dictionary<int, int>();
var newMats = new Material[usedIndices.Count];
int newIdx = 0;
foreach (int oldIdx in usedIndices)
{
newMats[newIdx] = oldIdx < currentMats.Length ? currentMats[oldIdx] : material;
remap[oldIdx] = newIdx;
newIdx++;
}
foreach (var f in allFacesList)
{
int si = (int)submeshIndexProp.GetValue(f);
if (remap.TryGetValue(si, out int mapped) && mapped != si)
submeshIndexProp.SetValue(f, mapped);
}
meshRenderer.sharedMaterials = newMats;
}
}
RefreshMesh(pbMesh);
return new SuccessResponse($"Set material on {faces.Length} face(s)", new
{
facesModified = faces.Length,
materialPath,
});
}
private static object SetFaceColor(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
var faces = GetFacesByIndices(pbMesh, props["faceIndices"] ?? props["face_indices"]);
var colorToken = props["color"];
if (colorToken == null)
return new ErrorResponse("color parameter is required ([r,g,b,a]).");
var color = VectorParsing.ParseColorOrDefault(colorToken);
Undo.RegisterCompleteObjectUndo(pbMesh, "Set Face Color");
var setColorMethod = _proBuilderMeshType.GetMethod("SetFaceColor",
BindingFlags.Instance | BindingFlags.Public);
if (setColorMethod == null)
return new ErrorResponse("SetFaceColor method not found.");
foreach (var face in faces)
setColorMethod.Invoke(pbMesh, new object[] { face, color });
RefreshMesh(pbMesh);
bool skipSwap = props["skipMaterialSwap"]?.Value<bool>() ?? props["skip_material_swap"]?.Value<bool>() ?? false;
if (!skipSwap)
{
var go = pbMesh.gameObject;
var renderer = go.GetComponent<Renderer>();
if (renderer != null && renderer.sharedMaterial != null &&
renderer.sharedMaterial.shader.name.Contains("Standard"))
{
var vcShader = Shader.Find("ProBuilder/Standard Vertex Color")
?? Shader.Find("ProBuilder/Diffuse Vertex Color")
?? Shader.Find("Sprites/Default");
if (vcShader != null)
{
var vcMat = new Material(vcShader);
renderer.sharedMaterial = vcMat;
}
}
}
return new SuccessResponse($"Set color on {faces.Length} face(s)", new
{
facesModified = faces.Length,
color = new[] { color.r, color.g, color.b, color.a },
});
}
private static object SetFaceUVs(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
var faces = GetFacesByIndices(pbMesh, props["faceIndices"] ?? props["face_indices"]);
Undo.RegisterCompleteObjectUndo(pbMesh, "Set Face UVs");
var uvProperty = _faceType.GetProperty("uv");
if (uvProperty == null)
return new ErrorResponse("Face.uv property not found.");
var autoUnwrapType = uvProperty.PropertyType;
// Resolve reflection members once outside the loop
var scaleField = autoUnwrapType.GetField("scale") ?? (MemberInfo)autoUnwrapType.GetProperty("scale");
var offsetField = autoUnwrapType.GetField("offset");
var rotField = autoUnwrapType.GetField("rotation");
var flipUField = autoUnwrapType.GetField("flipU");
var flipVField = autoUnwrapType.GetField("flipV");
var scaleToken = props["scale"];
var offsetToken = props["offset"];
var rotationToken = props["rotation"];
var flipUToken = props["flipU"] ?? props["flip_u"];
var flipVToken = props["flipV"] ?? props["flip_v"];
foreach (var face in faces)
{
var uvSettings = uvProperty.GetValue(face);
if (scaleToken != null && scaleField is FieldInfo scaleFi)
{
var scaleArr = scaleToken.ToObject<float[]>();
scaleFi.SetValue(uvSettings, new Vector2(scaleArr[0], scaleArr.Length > 1 ? scaleArr[1] : scaleArr[0]));
}
if (offsetToken != null && offsetField != null)
{
var offsetArr = offsetToken.ToObject<float[]>();
offsetField.SetValue(uvSettings, new Vector2(offsetArr[0], offsetArr.Length > 1 ? offsetArr[1] : 0f));
}
if (rotationToken != null && rotField != null)
rotField.SetValue(uvSettings, rotationToken.Value<float>());
if (flipUToken != null && flipUField != null)
flipUField.SetValue(uvSettings, flipUToken.Value<bool>());
if (flipVToken != null && flipVField != null)
flipVField.SetValue(uvSettings, flipVToken.Value<bool>());
uvProperty.SetValue(face, uvSettings);
}
var refreshUVMethod = _proBuilderMeshType.GetMethod("RefreshUV",
BindingFlags.Instance | BindingFlags.Public);
if (refreshUVMethod != null)
{
var allFaces = GetFacesArray(pbMesh);
refreshUVMethod.Invoke(pbMesh, new[] { allFaces });
}
RefreshMesh(pbMesh);
return new SuccessResponse($"Set UV parameters on {faces.Length} face(s)", new
{
facesModified = faces.Length,
});
}
// =====================================================================
// Query
// =====================================================================
private static object GetMeshInfo(JObject @params)
{
var pbMesh = RequireProBuilderMesh(@params);
var props = ExtractProperties(@params);
var include = (props["include"]?.ToString() ?? "summary").ToLowerInvariant();
var allFaces = GetFacesArray(pbMesh);
var facesList = (System.Collections.IList)allFaces;
var renderer = pbMesh.gameObject.GetComponent<MeshRenderer>();
Bounds bounds = renderer != null ? renderer.bounds : new Bounds();
var materials = new List<string>();
if (renderer != null)
{
foreach (var mat in renderer.sharedMaterials)
materials.Add(mat != null ? mat.name : "(none)");
}
var data = new Dictionary<string, object>
{
["gameObjectName"] = pbMesh.gameObject.name,
["instanceId"] = pbMesh.gameObject.GetInstanceIDCompat(),
["faceCount"] = GetFaceCount(pbMesh),
["vertexCount"] = GetVertexCount(pbMesh),
["bounds"] = new
{
center = new[] { bounds.center.x, bounds.center.y, bounds.center.z },
size = new[] { bounds.size.x, bounds.size.y, bounds.size.z },
},
["materials"] = materials,
};
if (include == "faces" || include == "all")
{
var positionsPropFaces = _proBuilderMeshType.GetProperty("positions");
var positionsListFaces = positionsPropFaces?.GetValue(pbMesh) as System.Collections.IList;
var indexesPropFaces = _faceType.GetProperty("indexes");
var smGroupProp = _faceType.GetProperty("smoothingGroup");
var manualUVProp = _faceType.GetProperty("manualUV");
var faceDetails = new List<object>();
for (int i = 0; i < facesList.Count && i < 100; i++)
{
var face = facesList[i];
var smGroup = smGroupProp?.GetValue(face);
var manualUV = manualUVProp?.GetValue(face);
var normal = ComputeFaceNormal(pbMesh, face, positionsListFaces, indexesPropFaces);
var center = ComputeFaceCenter(pbMesh, face, positionsListFaces, indexesPropFaces);
var direction = ClassifyDirection(normal);
faceDetails.Add(new
{
index = i,
smoothingGroup = smGroup,
manualUV = manualUV,
normal = new[] { Round(normal.x), Round(normal.y), Round(normal.z) },
center = new[] { Round(center.x), Round(center.y), Round(center.z) },
direction,
});
}
data["faces"] = faceDetails;
data["truncated"] = facesList.Count > 100;
}
if (include == "edges" || include == "all")
{
var uniqueEdges = CollectUniqueEdges(pbMesh);
// Get vertex positions for enriched edge data
var positionsProp = _proBuilderMeshType.GetProperty("positions");
var positions = positionsProp?.GetValue(pbMesh) as IList<Vector3>;
var edgeDetails = new List<object>();
for (int i = 0; i < uniqueEdges.Count && i < 200; i++)
{
var edge = uniqueEdges[i];
int vertA = GetEdgeVertexA(edge);
int vertB = GetEdgeVertexB(edge);
var edgeInfo = new Dictionary<string, object>
{
["index"] = i,
["vertexA"] = vertA,
["vertexB"] = vertB,
};
// Include world-space positions for each endpoint
if (positions != null)
{
if (vertA >= 0 && vertA < positions.Count)
{
var posA = pbMesh.transform.TransformPoint(positions[vertA]);
edgeInfo["positionA"] = new[] { Round(posA.x), Round(posA.y), Round(posA.z) };
}
if (vertB >= 0 && vertB < positions.Count)
{
var posB = pbMesh.transform.TransformPoint(positions[vertB]);
edgeInfo["positionB"] = new[] { Round(posB.x), Round(posB.y), Round(posB.z) };
}
}
edgeDetails.Add(edgeInfo);
}
data["edges"] = edgeDetails;
data["edgeCount"] = uniqueEdges.Count;
data["edgesTruncated"] = uniqueEdges.Count > 200;
}
return new SuccessResponse("ProBuilder mesh info", data);
}
private static Vector3 ComputeFaceNormal(Component pbMesh, object face,
System.Collections.IList positions = null, PropertyInfo indexesProp = null)
{
if (positions == null)
{
var positionsProp = _proBuilderMeshType.GetProperty("positions");
positions = positionsProp?.GetValue(pbMesh) as System.Collections.IList;
}
if (indexesProp == null)
indexesProp = _faceType.GetProperty("indexes");
var indexes = indexesProp?.GetValue(face) as System.Collections.IList;
if (positions == null || indexes == null || indexes.Count < 3)
return Vector3.up;
var p0 = (Vector3)positions[(int)indexes[0]];
var p1 = (Vector3)positions[(int)indexes[1]];
var p2 = (Vector3)positions[(int)indexes[2]];
var localNormal = Vector3.Cross(p1 - p0, p2 - p0).normalized;
return pbMesh.transform.rotation * localNormal;
}
private static Vector3 ComputeFaceCenter(Component pbMesh, object face,
System.Collections.IList positions = null, PropertyInfo indexesProp = null)
{
if (positions == null)
{
var positionsProp = _proBuilderMeshType.GetProperty("positions");
positions = positionsProp?.GetValue(pbMesh) as System.Collections.IList;
}
if (indexesProp == null)
indexesProp = _faceType.GetProperty("indexes");
var indexes = indexesProp?.GetValue(face) as System.Collections.IList;
if (positions == null || indexes == null || indexes.Count == 0)
return pbMesh.transform.position;
var sum = Vector3.zero;
foreach (int idx in indexes)
sum += (Vector3)positions[idx];
var localCenter = sum / indexes.Count;
return pbMesh.transform.TransformPoint(localCenter);
}
private static string ClassifyDirection(Vector3 normal)
{
var dirs = new (Vector3 dir, string label)[]
{
(Vector3.up, "top"),
(Vector3.down, "bottom"),
(Vector3.forward, "front"),
(Vector3.back, "back"),
(Vector3.left, "left"),
(Vector3.right, "right"),
};
foreach (var (dir, label) in dirs)
{
if (Vector3.Dot(normal, dir) > 0.7f)
return label;
}
return null;
}
internal static float Round(float v) => (float)Math.Round(v, 4);
private static object ConvertToProBuilder(JObject @params)
{
var go = FindTarget(@params);
if (go == null)
return new ErrorResponse("Target GameObject not found.");
var existingPB = GetProBuilderMesh(go);
if (existingPB != null)
return new ErrorResponse($"GameObject '{go.name}' already has a ProBuilderMesh component.");
var meshFilter = go.GetComponent<MeshFilter>();
if (meshFilter == null || meshFilter.sharedMesh == null)
return new ErrorResponse($"GameObject '{go.name}' does not have a MeshFilter with a valid mesh.");
if (_meshImporterType == null)
return new ErrorResponse("MeshImporter type not found.");
Undo.RegisterCompleteObjectUndo(go, "Convert to ProBuilder");
var pbMesh = go.AddComponent(_proBuilderMeshType);
// Use MeshImporter(Mesh, Material[], ProBuilderMesh) constructor
var renderer = go.GetComponent<MeshRenderer>();
var materials = renderer != null ? renderer.sharedMaterials : new Material[0];
var importerCtor = _meshImporterType.GetConstructor(
new[] { typeof(Mesh), typeof(Material[]), _proBuilderMeshType });
if (importerCtor == null)
{
// Fall back to MeshImporter(ProBuilderMesh)
importerCtor = _meshImporterType.GetConstructor(new[] { _proBuilderMeshType });
if (importerCtor == null)
return new ErrorResponse("MeshImporter constructor not found.");
}
object importer;
if (importerCtor.GetParameters().Length == 3)
importer = importerCtor.Invoke(new object[] { meshFilter.sharedMesh, materials, pbMesh });
else
importer = importerCtor.Invoke(new object[] { pbMesh });
// Find Import() overload with fewest parameters (takes optional MeshImportSettings)
var importM = _meshImporterType.GetMethods(BindingFlags.Instance | BindingFlags.Public)
.Where(m => m.Name == "Import")
.OrderBy(m => m.GetParameters().Length)
.FirstOrDefault();
if (importM != null)
{
var importParams = importM.GetParameters();
if (importParams.Length == 0)
importM.Invoke(importer, null);
else
importM.Invoke(importer, new object[] { null });
}
RefreshMesh(pbMesh);
return new SuccessResponse($"Converted '{go.name}' to ProBuilder", new
{
gameObjectName = go.name,
faceCount = GetFaceCount(pbMesh),
vertexCount = GetVertexCount(pbMesh),
});
}
}
}