More work on graph editing

This commit is contained in:
2026-03-13 00:48:19 -04:00
parent 8715cd25b0
commit 7202ef9bf6
8 changed files with 223 additions and 120 deletions

View File

@@ -6,6 +6,9 @@
#include "MCPUtils.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "Materials/Material.h"
#include "IMaterialEditor.h"
#include "GraphNode_Delete.generated.h"
@@ -47,8 +50,21 @@ public:
}
F.PreEdit();
FoundNode->BreakAllNodeLinks();
Graph->RemoveNode(FoundNode);
if (Cast<UMaterialGraphNode>(FoundNode))
{
// Use the material editor's DeleteNodes to properly remove
// both the graph node and the underlying material expression.
IMaterialEditor* MatEditor = F.CastEditor<UMaterial, IMaterialEditor>();
if (!MatEditor) return;
MatEditor->DeleteNodes({FoundNode});
}
else
{
FoundNode->BreakAllNodeLinks();
Graph->RemoveNode(FoundNode);
}
F.PostEdit();
Result.Appendf(TEXT("Deleted node '%s' from graph '%s'.\n"), *NodeTitle, *GraphName);

View File

@@ -0,0 +1,156 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "MaterialGraph/MaterialGraphSchema.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "GraphNode_SetDefaults.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FSetNodeDefaultEntry
{
GENERATED_BODY()
UPROPERTY()
FString Node;
UPROPERTY()
FString Name;
UPROPERTY()
FString Value;
};
UCLASS()
class UMCP_GraphNode_SetDefaults : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Graph path, e.g. /Game/Foo,graph:EventGraph"))
FString Graph;
UPROPERTY(meta=(Description="Array of {node, name, value} objects"))
FMCPJsonArray Pins;
virtual FString GetDescription() const override
{
return TEXT("Set the default value of input pins or material expression properties on nodes.");
}
// -----------------------------------------------------------------------
// K2 graphs: set pin default values.
// -----------------------------------------------------------------------
void HandleK2Entry(const FSetNodeDefaultEntry& Entry, UEdGraph* GraphObj, const UEdGraphSchema_K2* K2Schema,
TSet<UEdGraphNode*>& ModifiedNodes, FStringBuilderBase& Result)
{
MCPFetcher F(Result, GraphObj);
UEdGraphPin* Pin = F.Node(Entry.Node).Pin(Entry.Name).Cast<UEdGraphPin>();
if (!Pin) return;
UEdGraphNode* Node = Pin->GetOwningNode();
if (Pin->Direction != EGPD_Input)
{
Result.Appendf(TEXT("error: %s is an output pin\n"), *MCPUtils::FormatName(Pin));
return;
}
Pin->Modify();
FString UseDefaultValue;
TObjectPtr<UObject> UseDefaultObject = nullptr;
FText UseDefaultText;
K2Schema->GetPinDefaultValuesFromString(Pin->PinType, Node, Entry.Value, UseDefaultValue, UseDefaultObject, UseDefaultText, false);
FString Error = K2Schema->IsPinDefaultValid(Pin, UseDefaultValue, UseDefaultObject, UseDefaultText);
if (!Error.IsEmpty())
{
Result.Appendf(TEXT("error: %s: %s\n"), *MCPUtils::FormatName(Pin), *Error);
return;
}
K2Schema->TrySetDefaultValue(*Pin, Entry.Value);
ModifiedNodes.Add(Node);
}
// -----------------------------------------------------------------------
// Material graphs: set material expression properties.
// -----------------------------------------------------------------------
void HandleMaterialEntry(const FSetNodeDefaultEntry& Entry, UEdGraph* GraphObj,
TSet<UEdGraphNode*>& ModifiedNodes, FStringBuilderBase& Result)
{
MCPFetcher F(Result, GraphObj);
UMaterialGraphNode* MatNode = F.Node(Entry.Node).Cast<UMaterialGraphNode>();
if (!MatNode) return;
if (!MatNode->MaterialExpression)
{
Result.Appendf(TEXT("error: %s has no material expression\n"), *MCPUtils::FormatName(MatNode));
return;
}
UMaterialExpression* Expression = MatNode->MaterialExpression;
FProperty* Prop = MCPUtils::FindPropertyByName(Expression, Entry.Name, Result);
if (!Prop) return;
if (!MCPUtils::SetPropertyValueText(Expression, Prop, Entry.Value, Result))
return;
Expression->ForcePropertyValueChanged(Prop);
ModifiedNodes.Add(MatNode);
}
// -----------------------------------------------------------------------
virtual void Handle(FStringBuilderBase& Result) override
{
// Fetch the graph once.
MCPFetcher GraphFetcher(Result);
UEdGraph* GraphObj = GraphFetcher.Walk(Graph).Cast<UEdGraph>();
if (!GraphObj) return;
const UEdGraphSchema* Schema = GraphObj->GetSchema();
const UEdGraphSchema_K2* K2Schema = Cast<UEdGraphSchema_K2>(Schema);
const UMaterialGraphSchema* MGSchema = Cast<UMaterialGraphSchema>(Schema);
if (!K2Schema && !MGSchema)
{
Result.Appendf(TEXT("error: unsupported graph schema %s\n"), *Schema->GetClass()->GetName());
return;
}
TSet<UEdGraphNode*> ModifiedNodes;
GraphFetcher.PreEdit();
for (const TSharedPtr<FJsonValue>& PinVal : Pins.Array)
{
FSetNodeDefaultEntry Entry;
if (!MCPUtils::PopulateFromJson(FSetNodeDefaultEntry::StaticStruct(), &Entry, PinVal, Result))
continue;
if (K2Schema)
HandleK2Entry(Entry, GraphObj, K2Schema, ModifiedNodes, Result);
else if (MGSchema)
HandleMaterialEntry(Entry, GraphObj, ModifiedNodes, Result);
}
for (UEdGraphNode* Node : ModifiedNodes)
{
Node->ReconstructNode();
}
GraphFetcher.PostEdit();
Result.Appendf(TEXT("Done.\n"));
}
};

View File

@@ -1,114 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "GraphPin_SetDefault.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FSetPinDefaultEntry
{
GENERATED_BODY()
UPROPERTY()
FString Node;
UPROPERTY()
FString Pin;
UPROPERTY()
FString Value;
};
UCLASS()
class UMCP_GraphPin_SetDefault : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Graph path, e.g. /Game/Foo,graph:EventGraph"))
FString Graph;
UPROPERTY(meta=(Description="Array of {node, pin, value} objects"))
FMCPJsonArray Pins;
virtual FString GetDescription() const override
{
return TEXT("Set the default value of input pins on nodes.");
}
virtual void Handle(FStringBuilderBase& Result) override
{
// Fetch the graph once.
MCPFetcher GraphFetcher(Result);
UEdGraph* GraphObj = GraphFetcher.Walk(Graph).Cast<UEdGraph>();
if (!GraphObj) return;
int32 SuccessCount = 0;
int32 TotalCount = Pins.Array.Num();
TSet<UEdGraphNode*> ModifiedNodes;
GraphFetcher.PreEdit();
for (const TSharedPtr<FJsonValue>& PinVal : Pins.Array)
{
FSetPinDefaultEntry Entry;
if (!MCPUtils::PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal, Result))
continue;
MCPFetcher F(Result, GraphObj);
UEdGraphPin* Pin = F.Node(Entry.Node).Pin(Entry.Pin).Cast<UEdGraphPin>();
if (!Pin) continue;
UEdGraphNode* Node = Pin->GetOwningNode();
if (Pin->Direction != EGPD_Input)
{
Result.Appendf(TEXT("error: %s is an output pin\n"), *MCPUtils::FormatName(Pin));
continue;
}
Pin->Modify();
// K2 schemas support validation before setting; other schemas (e.g. material) don't.
const UEdGraphSchema_K2* K2Schema = Cast<UEdGraphSchema_K2>(Node->GetGraph()->GetSchema());
if (K2Schema)
{
FString UseDefaultValue;
TObjectPtr<UObject> UseDefaultObject = nullptr;
FText UseDefaultText;
K2Schema->GetPinDefaultValuesFromString(Pin->PinType, Node, Entry.Value, UseDefaultValue, UseDefaultObject, UseDefaultText, false);
FString Error = K2Schema->IsPinDefaultValid(Pin, UseDefaultValue, UseDefaultObject, UseDefaultText);
if (!Error.IsEmpty())
{
Result.Appendf(TEXT("error: %s: %s\n"), *MCPUtils::FormatName(Pin), *Error);
continue;
}
K2Schema->TrySetDefaultValue(*Pin, Entry.Value);
}
else
{
Node->GetGraph()->GetSchema()->TrySetDefaultValue(*Pin, Entry.Value);
}
SuccessCount++;
ModifiedNodes.Add(Node);
}
for (UEdGraphNode* Node : ModifiedNodes)
{
Node->ReconstructNode();
}
GraphFetcher.PostEdit();
Result.Appendf(TEXT("Set %d/%d pin defaults.\n"), SuccessCount, TotalCount);
}
};

View File

@@ -288,7 +288,7 @@ void FlxBlueprintExporter::EmitMaterialProperty(UMaterialExpression* Expression,
bool bEditable = !Prop->HasAnyPropertyFlags(CPF_EditConst);
Out.Appendf(TEXT(" %s %s %s = %s\n"),
bEditable ? TEXT("editable") : TEXT("readonly"),
bEditable ? TEXT("mxeditable") : TEXT("mxreadonly"),
*MCPUtils::FormatPropertyType(Prop),
*MCPUtils::FormatName(Prop),
*ValueStr);

View File

@@ -11,6 +11,7 @@
#include "MaterialGraph/MaterialGraph.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "Engine/LevelScriptBlueprint.h"
#include "Subsystems/AssetEditorSubsystem.h"
MCPFetcher& MCPFetcher::SetError(const FString& Msg)
{
@@ -86,7 +87,18 @@ MCPFetcher& MCPFetcher::Asset(const FString& PackagePath)
{
SetObj(LoadObject<UObject>(nullptr, *PackagePath));
if (!Obj)
SetError(FString::Printf(TEXT("Could not load asset '%s'"), *PackagePath));
return SetError(FString::Printf(TEXT("Could not load asset '%s'"), *PackagePath));
OriginalAsset = Obj;
// Open the editor for this asset (or bring it to front if already open).
UAssetEditorSubsystem* Sub = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
if (!Sub || !Sub->OpenEditorForAsset(Obj))
return SetError(FString::Printf(TEXT("Could not open editor for '%s'"), *PackagePath));
Editor = Sub->FindEditorForAsset(OriginalAsset, false);
if (!Editor)
return SetError(FString::Printf(TEXT("Could not find editor instance for '%s'"), *PackagePath));
// If this is a material open in the editor, use the editor's transient copy.
if (UMaterial* Mat = ::Cast<UMaterial>(Obj))

View File

@@ -1126,6 +1126,7 @@ void MCPUtils::PreEdit(const TArray<UObject*>& Objects)
void MCPUtils::PostEdit(const TArray<UObject*>& Objects)
{
TSet<UEdGraph*> Graphs;
TSet<UMaterial*> Materials;
TSet<UBlueprint*> Blueprints;
for (int32 i = Objects.Num() - 1; i >= 0; --i)
@@ -1134,6 +1135,9 @@ void MCPUtils::PostEdit(const TArray<UObject*>& Objects)
Obj->PostEditChange();
Obj->MarkPackageDirty();
if (UEdGraph* Graph = Cast<UEdGraph>(Obj))
Graphs.Add(Graph);
if (UBlueprint* BP = Cast<UBlueprint>(Obj))
Blueprints.Add(BP);
@@ -1141,6 +1145,8 @@ void MCPUtils::PostEdit(const TArray<UObject*>& Objects)
if (UMaterial* BaseMat = MatIface->GetMaterial())
Materials.Add(BaseMat);
}
for (UEdGraph* Graph : Graphs)
Graph->NotifyGraphChanged();
for (UMaterial *Material : Materials)
UMaterialEditingLibrary::RebuildMaterialInstanceEditors(Material);
for (UBlueprint *Blueprint : Blueprints)

View File

@@ -4,6 +4,7 @@
#include "MCPUtils.h"
class UEdGraphPin;
class IAssetEditorInstance;
// Resolves a path string into a UObject or UEdGraphPin.
//
@@ -29,6 +30,8 @@ class MCPFetcher
public:
bool bError = false;
UObject* Obj = nullptr;
UObject* OriginalAsset = nullptr;
IAssetEditorInstance* Editor = nullptr;
UEdGraphPin* ResultPin = nullptr;
MCPErrorCallback ErrorCB = nullptr;
@@ -69,6 +72,30 @@ public:
static const TArray<FWalker>& GetWalkerTable();
bool Ok() const { return !bError; }
UObject* GetAsset() const { return OriginalAsset; }
template<class T> T* CastAsset()
{
if (bError) return nullptr;
T* Result = ::Cast<T>(OriginalAsset);
if (!Result)
SetError(FString::Printf(TEXT("Asset is %s, expected %s"),
OriginalAsset ? *OriginalAsset->GetClass()->GetName() : TEXT("null"),
*T::StaticClass()->GetName()));
return Result;
}
template<class AssetType, class EditorType>
EditorType* CastEditor()
{
if (bError) return nullptr;
if (!OriginalAsset || !OriginalAsset->IsA<AssetType>())
{
SetError(FString::Printf(TEXT("Asset is %s, expected %s"),
OriginalAsset ? *OriginalAsset->GetClass()->GetName() : TEXT("null"),
*AssetType::StaticClass()->GetName()));
return nullptr;
}
return static_cast<EditorType*>(Editor);
}
const TArray<UObject*>& Visited() const { return Chain; }
void PreEdit() { MCPUtils::PreEdit(Chain); }
void PostEdit() { MCPUtils::PostEdit(Chain); }