#pragma once #include "CoreMinimal.h" #include "MCPHandler.h" #include "MCPAssets.h" #include "MCPFetcher.h" #include "MCPUtils.h" #include "Materials/Material.h" #include "Materials/MaterialExpression.h" #include "Materials/MaterialFunction.h" #include "MaterialGraph/MaterialGraph.h" #include "MaterialGraph/MaterialGraphNode.h" #include "UMCPHandler_SetMaterialExpressionPosition.generated.h" // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- UCLASS(meta=(Group="Unclassified")) class UMCPHandler_SetMaterialExpressionPosition : public UObject, public IMCPHandler { GENERATED_BODY() public: UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)")) FString Material; UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)")) FString MaterialFunction; UPROPERTY(meta=(Description="Expression name (use FormatName from DumpMaterial output)")) FString Node; UPROPERTY(meta=(Description="New X position")) int32 PosX = 0; UPROPERTY(meta=(Description="New Y position")) int32 PosY = 0; UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it")) bool DryRun = false; virtual FString GetDescription() const override { return TEXT("Reposition a material expression node in the material graph editor."); } virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override { if (Material.IsEmpty() && MaterialFunction.IsEmpty()) { MCPErrorCallback(Result).SetError(TEXT("Specify 'material' or 'materialFunction'.")); return; } // Load material or material function UMaterial* MaterialObj = nullptr; UMaterialFunction* MatFunc = nullptr; if (!MaterialFunction.IsEmpty()) { MCPAssets MFAssets; if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return; MatFunc = MFAssets.Object(); } else { MCPAssets MatAssets; if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return; MaterialObj = MatAssets.Object(); } if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj); UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr); if (!Graph) { MCPErrorCallback(Result).SetError(TEXT("Asset has no material graph.")); return; } // Find node by name UMaterialGraphNode* TargetMatNode = nullptr; for (UEdGraphNode* GraphNode : Graph->Nodes) { if (!GraphNode) continue; UMaterialGraphNode* MatNode = Cast(GraphNode); if (!MatNode || !MatNode->MaterialExpression) continue; if (MCPUtils::Identifies(Node, MatNode->MaterialExpression)) { TargetMatNode = MatNode; break; } } if (!TargetMatNode) { MCPErrorCallback(Result).SetError(FString::Printf(TEXT("Expression '%s' not found."), *Node)); return; } if (DryRun) { Result.Appendf(TEXT("DryRun: would move %s to (%d, %d)\n"), *MCPUtils::FormatName(TargetMatNode->MaterialExpression), PosX, PosY); return; } // Set position on the graph node TargetMatNode->NodePosX = PosX; TargetMatNode->NodePosY = PosY; // Also update the underlying expression position if (TargetMatNode->MaterialExpression) { TargetMatNode->MaterialExpression->MaterialExpressionEditorX = PosX; TargetMatNode->MaterialExpression->MaterialExpressionEditorY = PosY; } UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc; Asset->PreEditChange(nullptr); Asset->PostEditChange(); // Save bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc); Result.Appendf(TEXT("Moved %s to (%d, %d)"), *MCPUtils::FormatName(TargetMatNode->MaterialExpression), PosX, PosY); if (!bSaved) Result.Append(TEXT(" (save failed)")); Result.Append(TEXT("\n")); } };