From 083f8db0a78efca93d9837c2039634179e00bda7 Mon Sep 17 00:00:00 2001
From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com>
Date: Wed, 4 Mar 2026 22:19:59 -0500
Subject: [PATCH 1/8] ProBuilder support initial update
---
.../references/tools-reference.md | 88 +
.../Tools/GameObjects/GameObjectModify.cs | 2 +-
MCPForUnity/Editor/Tools/ManageScene.cs | 16 +-
MCPForUnity/Editor/Tools/ProBuilder.meta | 8 +
.../Tools/ProBuilder/ManageProBuilder.cs | 1784 +++++++++++++++++
.../Tools/ProBuilder/ManageProBuilder.cs.meta | 11 +
.../Tools/ProBuilder/ProBuilderMeshUtils.cs | 254 +++
.../ProBuilder/ProBuilderMeshUtils.cs.meta | 11 +
.../Tools/ProBuilder/ProBuilderSmoothing.cs | 97 +
.../ProBuilder/ProBuilderSmoothing.cs.meta | 11 +
README.md | 2 +-
Server/src/cli/commands/probuilder.py | 341 ++++
Server/src/cli/main.py | 1 +
Server/src/services/registry/tool_registry.py | 1 +
Server/src/services/tools/manage_material.py | 2 +-
.../src/services/tools/manage_probuilder.py | 176 ++
Server/tests/test_manage_probuilder.py | 465 +++++
Server/uv.lock | 12 +-
.../EditMode/Tools/ManageProBuilderTests.cs | 860 ++++++++
.../Tools/ManageProBuilderTests.cs.meta | 11 +
docs/guides/CLI_USAGE.md | 23 +
21 files changed, 4168 insertions(+), 8 deletions(-)
create mode 100644 MCPForUnity/Editor/Tools/ProBuilder.meta
create mode 100644 MCPForUnity/Editor/Tools/ProBuilder/ManageProBuilder.cs
create mode 100644 MCPForUnity/Editor/Tools/ProBuilder/ManageProBuilder.cs.meta
create mode 100644 MCPForUnity/Editor/Tools/ProBuilder/ProBuilderMeshUtils.cs
create mode 100644 MCPForUnity/Editor/Tools/ProBuilder/ProBuilderMeshUtils.cs.meta
create mode 100644 MCPForUnity/Editor/Tools/ProBuilder/ProBuilderSmoothing.cs
create mode 100644 MCPForUnity/Editor/Tools/ProBuilder/ProBuilderSmoothing.cs.meta
create mode 100644 Server/src/cli/commands/probuilder.py
create mode 100644 Server/src/services/tools/manage_probuilder.py
create mode 100644 Server/tests/test_manage_probuilder.py
create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageProBuilderTests.cs
create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageProBuilderTests.cs.meta
diff --git a/.claude/skills/unity-mcp-skill/references/tools-reference.md b/.claude/skills/unity-mcp-skill/references/tools-reference.md
index 60b967cd4..cdd644bbc 100644
--- a/.claude/skills/unity-mcp-skill/references/tools-reference.md
+++ b/.claude/skills/unity-mcp-skill/references/tools-reference.md
@@ -15,6 +15,7 @@ Complete reference for all MCP tools. Each tool includes parameters, types, and
- [UI Tools](#ui-tools)
- [Editor Control Tools](#editor-control-tools)
- [Testing Tools](#testing-tools)
+- [ProBuilder Tools](#probuilder-tools)
---
@@ -789,3 +790,90 @@ execute_custom_tool(
```
Discover available custom tools via `mcpforunity://custom-tools` resource.
+
+---
+
+## ProBuilder Tools
+
+### manage_probuilder
+
+Unified tool for ProBuilder mesh operations. Requires `com.unity.probuilder` package.
+
+**Parameters:**
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `action` | string | Yes | Action to perform (see categories below) |
+| `target` | string | Sometimes | Target GameObject name/path/id |
+| `search_method` | string | No | How to find target: `by_id`, `by_name`, `by_path`, `by_tag`, `by_layer` |
+| `properties` | dict | No | Action-specific parameters |
+
+**Actions by category:**
+
+**Shape Creation:**
+- `create_shape` — Create ProBuilder primitive (shape_type, size, position, rotation, name)
+- `create_poly_shape` — Create from 2D polygon footprint (points, extrudeHeight, flipNormals)
+
+**Mesh Editing:**
+- `extrude_faces` — Extrude faces (faceIndices, distance, method)
+- `extrude_edges` — Extrude edges (edgeIndices, distance, asGroup)
+- `bevel_edges` — Bevel edges (edgeIndices, 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)
+- `connect_elements` — Connect edges/faces (edgeIndices or faceIndices)
+- `detach_faces` — Detach faces to new object (faceIndices, deleteSource)
+- `flip_normals` — Flip face normals (faceIndices)
+- `merge_faces` — Merge faces into one (faceIndices)
+- `combine_meshes` — Combine ProBuilder objects (targets list)
+
+**Vertex Operations:**
+- `merge_vertices` — Merge/weld vertices (vertexIndices)
+- `split_vertices` — Split shared vertices (vertexIndices)
+- `move_vertices` — Translate vertices (vertexIndices, offset [x,y,z])
+
+**UV & Materials:**
+- `set_face_material` — Assign material to faces (faceIndices, materialPath)
+- `set_face_color` — Set vertex color on faces (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 with `include` parameter:
+ - `"summary"` (default): counts, bounds, materials
+ - `"faces"`: + face normals, centers, and direction labels
+ - `"edges"`: + edge vertex pairs (capped at 200)
+ - `"all"`: everything
+- `convert_to_probuilder` — Convert standard mesh to ProBuilder
+
+**Smoothing:**
+- `set_smoothing` — Set smoothing group on faces (faceIndices, smoothingGroup: 0=hard, 1+=smooth)
+- `auto_smooth` — Auto-assign smoothing groups by angle (angleThreshold: default 30)
+
+**Mesh Utilities:**
+- `center_pivot` — Move pivot to mesh bounds center
+- `freeze_transform` — Bake transform into vertices, reset transform
+- `validate_mesh` — Check mesh health (read-only diagnostics)
+- `repair_mesh` — Auto-fix degenerate triangles
+
+**Examples:**
+
+```python
+# Create a cube
+manage_probuilder(action="create_shape", properties={"shape_type": "Cube", "name": "MyCube"})
+
+# Get face info with directions
+manage_probuilder(action="get_mesh_info", target="MyCube", properties={"include": "faces"})
+
+# Extrude the top face (find it via direction="top" in get_mesh_info results)
+manage_probuilder(action="extrude_faces", target="MyCube",
+ properties={"faceIndices": [2], "distance": 1.5})
+
+# Auto-smooth
+manage_probuilder(action="auto_smooth", target="MyCube", properties={"angleThreshold": 30})
+
+# Cleanup workflow
+manage_probuilder(action="center_pivot", target="MyCube")
+manage_probuilder(action="validate_mesh", target="MyCube")
+```
+
+See also: [ProBuilder Workflow Guide](probuilder-guide.md) for detailed patterns.
diff --git a/MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs b/MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs
index 44511e91e..7be8f91dd 100644
--- a/MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs
+++ b/MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs
@@ -34,7 +34,7 @@ internal static object Handle(JObject @params, JToken targetToken, string search
bool modified = false;
- string name = @params["name"]?.ToString();
+ string name = @params["name"]?.ToString() ?? @params["new_name"]?.ToString() ?? @params["newName"]?.ToString();
if (!string.IsNullOrEmpty(name) && targetGo.name != name)
{
// Check if we're renaming the root object of an open prefab stage
diff --git a/MCPForUnity/Editor/Tools/ManageScene.cs b/MCPForUnity/Editor/Tools/ManageScene.cs
index ec59533ae..31a6e2a1e 100644
--- a/MCPForUnity/Editor/Tools/ManageScene.cs
+++ b/MCPForUnity/Editor/Tools/ManageScene.cs
@@ -604,8 +604,8 @@ private static object CaptureSurroundBatch(SceneCommand cmd)
if (r != null && r.gameObject.activeInHierarchy) targetBounds.Encapsulate(r.bounds);
}
center = targetBounds.center;
- radius = targetBounds.extents.magnitude * 1.8f;
- radius = Mathf.Max(radius, 3f);
+ radius = targetBounds.extents.magnitude * 2.5f;
+ radius = Mathf.Max(radius, 5f);
}
}
else
@@ -632,8 +632,8 @@ private static object CaptureSurroundBatch(SceneCommand cmd)
return new ErrorResponse("No renderers found in the scene. Cannot determine scene bounds for batch capture.");
center = bounds.center;
- radius = bounds.extents.magnitude * 1.8f;
- radius = Mathf.Max(radius, 3f);
+ radius = bounds.extents.magnitude * 2.5f;
+ radius = Mathf.Max(radius, 5f);
}
// Define 6 viewpoints: front, back, left, right, top, bird's-eye (45° elevated front-right)
@@ -665,6 +665,10 @@ private static object CaptureSurroundBatch(SceneCommand cmd)
tempCam.transform.position = pos;
tempCam.transform.LookAt(center);
+ // Force material refresh before capture
+ EditorApplication.QueuePlayerLoopUpdate();
+ SceneView.RepaintAll();
+
Texture2D tile = ScreenshotUtility.RenderCameraToTexture(tempCam, maxRes);
tiles.Add(tile);
tileLabels.Add(label);
@@ -804,6 +808,10 @@ private static object CaptureOrbitBatch(SceneCommand cmd)
: "level";
string angleLabel = $"{dirLabel}_{elevLabel}";
+ // Force material refresh before capture
+ EditorApplication.QueuePlayerLoopUpdate();
+ SceneView.RepaintAll();
+
Texture2D tile = ScreenshotUtility.RenderCameraToTexture(tempCam, maxRes);
tiles.Add(tile);
tileLabels.Add(angleLabel);
diff --git a/MCPForUnity/Editor/Tools/ProBuilder.meta b/MCPForUnity/Editor/Tools/ProBuilder.meta
new file mode 100644
index 000000000..c19b7ece6
--- /dev/null
+++ b/MCPForUnity/Editor/Tools/ProBuilder.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: bfd453a23cda46348e276fe627e5016f
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Tools/ProBuilder/ManageProBuilder.cs b/MCPForUnity/Editor/Tools/ProBuilder/ManageProBuilder.cs
new file mode 100644
index 000000000..2da47821f
--- /dev/null
+++ b/MCPForUnity/Editor/Tools/ProBuilder/ManageProBuilder.cs
@@ -0,0 +1,1784 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Newtonsoft.Json.Linq;
+using MCPForUnity.Editor.Helpers;
+using UnityEditor;
+using UnityEngine;
+
+namespace MCPForUnity.Editor.Tools.ProBuilder
+{
+ ///
+ /// 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 (shapeType, size/radius/height, position, rotation, name)
+ /// Shape types: Cube, Cylinder, Sphere, Plane, Cone, Torus, Pipe, Arch, Stair, CurvedStair, Door, Prism
+ /// - 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, distance, asGroup)
+ /// - bevel_edges: Bevel edges (edgeIndices, 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)
+ /// - connect_elements: Connect edges/faces (edgeIndices or faceIndices)
+ /// - detach_faces: Detach faces to new object (faceIndices, deleteSource)
+ /// - 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)
+ ///
+ /// VERTEX OPERATIONS:
+ /// - merge_vertices: Merge/weld vertices (vertexIndices)
+ /// - split_vertices: Split shared vertices (vertexIndices)
+ /// - move_vertices: Translate vertices (vertexIndices, offset [x,y,z])
+ ///
+ /// UV & 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)
+ /// - convert_to_probuilder: Convert standard mesh to ProBuilder
+ ///
+ [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 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");
+
+ // 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;
+ return true;
+ }
+
+ 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);
+
+ // Vertex operations
+ case "merge_vertices": return MergeVertices(@params);
+ case "split_vertices": return SplitVertices(@params);
+ case "move_vertices": return MoveVertices(@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 "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)
+ {
+ _proBuilderMeshType.GetMethod("ToMesh", Type.EmptyTypes)?.Invoke(pbMesh, null);
+ _proBuilderMeshType.GetMethod("Refresh", Type.EmptyTypes)?.Invoke(pbMesh, 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();
+ 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;
+ }
+
+ // =====================================================================
+ // 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.");
+
+ // Parse shape type enum
+ object shapeTypeValue;
+ try
+ {
+ shapeTypeValue = Enum.Parse(_shapeTypeEnum, shapeTypeStr, true);
+ }
+ catch
+ {
+ var validTypes = string.Join(", ", Enum.GetNames(_shapeTypeEnum));
+ return new ErrorResponse($"Unknown shape type '{shapeTypeStr}'. Valid types: {validTypes}");
+ }
+
+ // Use ShapeGenerator.CreateShape(ShapeType) or CreateShape(ShapeType, PivotLocation)
+ var createMethod = _shapeGeneratorType.GetMethod("CreateShape",
+ BindingFlags.Static | BindingFlags.Public,
+ null,
+ new[] { _shapeTypeEnum },
+ null);
+
+ // Fallback: look for overload with PivotLocation (ProBuilder 4.x+)
+ object[] invokeArgs;
+ if (createMethod != null)
+ {
+ invokeArgs = new[] { shapeTypeValue };
+ }
+ else
+ {
+ var pivotLocationType = Type.GetType("UnityEngine.ProBuilder.PivotLocation, Unity.ProBuilder");
+ if (pivotLocationType != null)
+ {
+ createMethod = _shapeGeneratorType.GetMethod("CreateShape",
+ BindingFlags.Static | BindingFlags.Public,
+ null,
+ new[] { _shapeTypeEnum, pivotLocationType },
+ null);
+ // PivotLocation.Center = 0
+ invokeArgs = new[] { shapeTypeValue, Enum.ToObject(pivotLocationType, 0) };
+ }
+ else
+ {
+ invokeArgs = null;
+ }
+ }
+
+ if (createMethod == null)
+ return new ErrorResponse("ShapeGenerator.CreateShape method not found. Check your ProBuilder version.");
+
+ Undo.IncrementCurrentGroup();
+ var pbMesh = createMethod.Invoke(null, invokeArgs) as Component;
+ if (pbMesh == null)
+ return new ErrorResponse("Failed to create ProBuilder shape.");
+
+ 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);
+
+ // Apply size/dimensions via scale (ShapeGenerator creates shapes with known defaults)
+ ApplyShapeDimensions(go, shapeTypeStr, props);
+
+ RefreshMesh(pbMesh);
+
+ return new SuccessResponse($"Created ProBuilder {shapeTypeStr}: {go.name}", new
+ {
+ gameObjectName = go.name,
+ instanceId = go.GetInstanceID(),
+ shapeType = shapeTypeStr,
+ faceCount = GetFaceCount(pbMesh),
+ vertexCount = GetVertexCount(pbMesh),
+ });
+ }
+
+ private static void ApplyShapeDimensions(GameObject go, string shapeType, JObject props)
+ {
+ float size = props["size"]?.Value() ?? 0;
+ float width = props["width"]?.Value() ?? 0;
+ float height = props["height"]?.Value() ?? 0;
+ float depth = props["depth"]?.Value() ?? 0;
+ float radius = props["radius"]?.Value() ?? 0;
+
+ if (size <= 0 && width <= 0 && height <= 0 && depth <= 0 && radius <= 0)
+ return;
+
+ // Each shape type has known default dimensions from ProBuilder's ShapeGenerator.
+ // We compute a scale factor relative to those defaults.
+ Vector3 scale;
+ string shapeUpper = shapeType.ToUpperInvariant();
+
+ switch (shapeUpper)
+ {
+ case "CUBE":
+ // Default: 1x1x1
+ scale = new Vector3(
+ width > 0 ? width : (size > 0 ? size : 1f),
+ height > 0 ? height : (size > 0 ? size : 1f),
+ depth > 0 ? depth : (size > 0 ? size : 1f));
+ break;
+
+ case "PRISM":
+ // Default: 1x1x1
+ scale = new Vector3(
+ width > 0 ? width : (size > 0 ? size : 1f),
+ height > 0 ? height : (size > 0 ? size : 1f),
+ depth > 0 ? depth : (size > 0 ? size : 1f));
+ break;
+
+ case "CYLINDER":
+ // Default: radius=0.5 (diameter=1), height=2
+ float cylRadius = radius > 0 ? radius : (size > 0 ? size / 2f : 0.5f);
+ float cylHeight = height > 0 ? height : (size > 0 ? size : 2f);
+ scale = new Vector3(cylRadius / 0.5f, cylHeight / 2f, cylRadius / 0.5f);
+ break;
+
+ case "CONE":
+ // Default: 1x1x1 (radius 0.5)
+ float coneRadius = radius > 0 ? radius : (size > 0 ? size / 2f : 0.5f);
+ float coneHeight = height > 0 ? height : (size > 0 ? size : 1f);
+ scale = new Vector3(coneRadius / 0.5f, coneHeight, coneRadius / 0.5f);
+ break;
+
+ case "SPHERE":
+ // Default: radius=0.5 (diameter=1)
+ float sphereRadius = radius > 0 ? radius : (size > 0 ? size / 2f : 0.5f);
+ scale = Vector3.one * (sphereRadius / 0.5f);
+ break;
+
+ case "TORUS":
+ // Default: fits in ~1x1x1
+ float torusScale = radius > 0 ? radius * 2f : (size > 0 ? size : 1f);
+ scale = Vector3.one * torusScale;
+ break;
+
+ case "ARCH":
+ // Default: approximately 4x2x1
+ scale = new Vector3(
+ width > 0 ? width / 4f : (size > 0 ? size / 4f : 1f),
+ height > 0 ? height / 2f : (size > 0 ? size / 2f : 1f),
+ depth > 0 ? depth : (size > 0 ? size : 1f));
+ break;
+
+ case "STAIR":
+ // Default: approximately 2x2.5x4
+ scale = new Vector3(
+ width > 0 ? width / 2f : (size > 0 ? size / 2f : 1f),
+ height > 0 ? height / 2.5f : (size > 0 ? size / 2.5f : 1f),
+ depth > 0 ? depth / 4f : (size > 0 ? size / 4f : 1f));
+ break;
+
+ case "CURVEDSTAIR":
+ // Default: similar to stair
+ scale = new Vector3(
+ width > 0 ? width / 2f : (size > 0 ? size / 2f : 1f),
+ height > 0 ? height / 2.5f : (size > 0 ? size / 2.5f : 1f),
+ depth > 0 ? depth / 2f : (size > 0 ? size / 2f : 1f));
+ break;
+
+ case "PIPE":
+ // Default: radius=1, height=2
+ float pipeRadius = radius > 0 ? radius : (size > 0 ? size / 2f : 1f);
+ float pipeHeight = height > 0 ? height : (size > 0 ? size : 2f);
+ scale = new Vector3(pipeRadius, pipeHeight / 2f, pipeRadius);
+ break;
+
+ case "PLANE":
+ // Default: 1x1
+ float planeSize = size > 0 ? size : 1f;
+ scale = new Vector3(
+ width > 0 ? width : planeSize,
+ 1f,
+ depth > 0 ? depth : planeSize);
+ break;
+
+ case "DOOR":
+ // Default: approximately 4x4x1
+ scale = new Vector3(
+ width > 0 ? width / 4f : (size > 0 ? size / 4f : 1f),
+ height > 0 ? height / 4f : (size > 0 ? size / 4f : 1f),
+ depth > 0 ? depth : (size > 0 ? size : 1f));
+ break;
+
+ default:
+ // Generic fallback: uniform scale from size
+ if (size > 0)
+ scale = Vector3.one * size;
+ else
+ return; // No dimensions to apply
+ break;
+ }
+
+ go.transform.localScale = scale;
+ }
+
+ 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();
+ 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() ?? props["extrude_height"]?.Value() ?? 1f;
+ bool flipNormals = props["flipNormals"]?.Value() ?? props["flip_normals"]?.Value() ?? false;
+
+ // Create a new GameObject with ProBuilderMesh
+ var go = new GameObject("PolyShape");
+ Undo.RegisterCreatedObjectUndo(go, "Create ProBuilder PolyShape");
+ var pbMesh = go.AddComponent(_proBuilderMeshType);
+
+ // Use AppendElements.CreateShapeFromPolygon
+ 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), typeof(float), typeof(bool) },
+ null);
+
+ if (createFromPolygonMethod == null)
+ {
+ UnityEngine.Object.DestroyImmediate(go);
+ return new ErrorResponse("CreateShapeFromPolygon method not found.");
+ }
+
+ var actionResult = 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.GetInstanceID(),
+ 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() ?? 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);
+ var edgeIndicesToken = props["edgeIndices"] ?? props["edge_indices"];
+ if (edgeIndicesToken == null)
+ return new ErrorResponse("edgeIndices parameter is required.");
+
+ float distance = props["distance"]?.Value() ?? 0.5f;
+ bool asGroup = props["asGroup"]?.Value() ?? props["as_group"]?.Value() ?? true;
+
+ var edgeIndices = edgeIndicesToken.ToObject();
+
+ // Get edges from the mesh
+ var edgesProperty = _proBuilderMeshType.GetProperty("faces");
+ var allFaces = (System.Collections.IList)edgesProperty?.GetValue(pbMesh);
+ if (allFaces == null)
+ return new ErrorResponse("Could not read faces from mesh.");
+
+ // Collect edges from specified indices
+ var edgeList = new List