Unified, portable Unreal Engine MCP system plugin
Merge of the two project copies into one self-contained plugin (the superset: variable_op + variables.py, full pcg_op runtime/declarative/preset ops, the CreateWidgetFloatAnimation widget tool, and full Voxel graph authoring). Made fully project- and machine-agnostic — no hardcoded paths: - New src/projectPaths.js auto-detects the host .uproject (walk-up), project name, Editor build target, log file, and engine install (EngineAssociation via launcher manifest/registry, else installed-engine scan). All overridable via UE_* env vars. - Rewired buildOrchestrator/insights/launcher/insightsExporter/server.js and the Python workers (cpp_scaffold, live_coding, apply_graph, console) off the old C:/Github/ihy, IHY*, E:/UE_Versions and UE_5.7 literals onto the resolver. - Voxel made optional: Build.cs auto-detects the Voxel plugin (env UE_MCP_WITH_VOXEL override) and the C++ compiles to stubs under WITH_MCP_VOXEL, so the module builds in projects without Voxel; .uplugin marks Voxel optional. - De-branded the agent-gateway and docs; scrubbed a leaked API key; excluded node_modules/Binaries/Intermediate/__pycache__/secrets from the repo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
202
Source/UEBlueprintMCPEditor/Private/MCPCaptureLibrary.cpp
Normal file
202
Source/UEBlueprintMCPEditor/Private/MCPCaptureLibrary.cpp
Normal file
@ -0,0 +1,202 @@
|
||||
#include "MCPCaptureLibrary.h"
|
||||
|
||||
#include "CanvasTypes.h"
|
||||
#include "Components/SceneCaptureComponent2D.h"
|
||||
#include "Editor.h"
|
||||
#include "Engine/TextureRenderTarget2D.h"
|
||||
#include "Engine/World.h"
|
||||
#include "HAL/PlatformFileManager.h"
|
||||
#include "IImageWrapper.h"
|
||||
#include "IImageWrapperModule.h"
|
||||
#include "Misc/App.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "ObjectTools.h"
|
||||
#include "RenderingThread.h"
|
||||
#include "Misc/ObjectThumbnail.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// Encode an array of BGRA8 pixels to a PNG on disk.
|
||||
bool SaveBGRAToPNG(
|
||||
const uint8* BGRA,
|
||||
int32 NumBytes,
|
||||
int32 Width,
|
||||
int32 Height,
|
||||
const FString& OutputAbsPath,
|
||||
const FString& FileNameNoExt,
|
||||
FString& OutFullPath,
|
||||
FString& OutError)
|
||||
{
|
||||
if (!BGRA || NumBytes < Width * Height * 4)
|
||||
{
|
||||
OutError = TEXT("empty / undersized pixel buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
IImageWrapperModule& Mod =
|
||||
FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
|
||||
TSharedPtr<IImageWrapper> PNG = Mod.CreateImageWrapper(EImageFormat::PNG);
|
||||
if (!PNG.IsValid() ||
|
||||
!PNG->SetRaw(BGRA, Width * Height * 4, Width, Height, ERGBFormat::BGRA, 8))
|
||||
{
|
||||
OutError = TEXT("PNG SetRaw failed");
|
||||
return false;
|
||||
}
|
||||
const TArray64<uint8>& Compressed = PNG->GetCompressed(100);
|
||||
|
||||
IPlatformFile& PF = FPlatformFileManager::Get().GetPlatformFile();
|
||||
if (!PF.DirectoryExists(*OutputAbsPath))
|
||||
{
|
||||
PF.CreateDirectoryTree(*OutputAbsPath);
|
||||
}
|
||||
|
||||
const FString FullPath = FPaths::Combine(OutputAbsPath, FileNameNoExt + TEXT(".png"));
|
||||
if (!FFileHelper::SaveArrayToFile(Compressed, *FullPath))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("SaveArrayToFile failed: %s"), *FullPath);
|
||||
return false;
|
||||
}
|
||||
OutFullPath = FullPath;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool UMCPCaptureLibrary::RenderAssetThumbnailToPNG(
|
||||
const FString& AssetPath,
|
||||
const FString& OutputAbsPath,
|
||||
const FString& FileNameNoExt,
|
||||
int32 Width,
|
||||
int32 Height,
|
||||
FString& OutFullPath,
|
||||
FString& OutError)
|
||||
{
|
||||
OutFullPath.Reset();
|
||||
OutError.Reset();
|
||||
|
||||
const int32 W = FMath::Clamp(Width, 16, 2048);
|
||||
const int32 H = FMath::Clamp(Height, 16, 2048);
|
||||
|
||||
UObject* Object = StaticLoadObject(UObject::StaticClass(), nullptr, *AssetPath);
|
||||
if (!Object)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("could not load asset: %s"), *AssetPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Render the asset through its registered thumbnail renderer. This is exactly
|
||||
// what the Content Browser uses, so it works for every asset type and needs
|
||||
// no open editor. RenderThumbnail allocates its own render target when we
|
||||
// pass null and fills the thumbnail with uncompressed BGRA8 pixels.
|
||||
FObjectThumbnail Thumbnail;
|
||||
ThumbnailTools::RenderThumbnail(
|
||||
Object,
|
||||
static_cast<uint32>(W),
|
||||
static_cast<uint32>(H),
|
||||
ThumbnailTools::EThumbnailTextureFlushMode::AlwaysFlush,
|
||||
nullptr,
|
||||
&Thumbnail);
|
||||
|
||||
const TArray<uint8>& Bytes = Thumbnail.GetUncompressedImageData();
|
||||
if (Bytes.Num() == 0)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("thumbnail renderer produced no pixels for %s (no renderer for this asset type?)"),
|
||||
*AssetPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
return SaveBGRAToPNG(
|
||||
Bytes.GetData(),
|
||||
Bytes.Num(),
|
||||
Thumbnail.GetImageWidth(),
|
||||
Thumbnail.GetImageHeight(),
|
||||
OutputAbsPath,
|
||||
FileNameNoExt,
|
||||
OutFullPath,
|
||||
OutError);
|
||||
}
|
||||
|
||||
bool UMCPCaptureLibrary::CaptureEditorSceneToPNG(
|
||||
FVector Location,
|
||||
FRotator Rotation,
|
||||
float FOV,
|
||||
const FString& OutputAbsPath,
|
||||
const FString& FileNameNoExt,
|
||||
int32 Width,
|
||||
int32 Height,
|
||||
FString& OutFullPath,
|
||||
FString& OutError)
|
||||
{
|
||||
OutFullPath.Reset();
|
||||
OutError.Reset();
|
||||
|
||||
UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
|
||||
if (!World)
|
||||
{
|
||||
OutError = TEXT("no editor world available");
|
||||
return false;
|
||||
}
|
||||
|
||||
const int32 W = FMath::Clamp(Width, 16, 4096);
|
||||
const int32 H = FMath::Clamp(Height, 16, 4096);
|
||||
|
||||
UTextureRenderTarget2D* RT = NewObject<UTextureRenderTarget2D>(GetTransientPackage());
|
||||
RT->ClearColor = FLinearColor::Black;
|
||||
RT->TargetGamma = 2.2f;
|
||||
RT->InitCustomFormat(W, H, PF_B8G8R8A8, /*bForceLinearGamma=*/false);
|
||||
RT->UpdateResourceImmediate(true);
|
||||
|
||||
// Transient capture component bound to the editor world — no actor, no viewport.
|
||||
USceneCaptureComponent2D* Capture =
|
||||
NewObject<USceneCaptureComponent2D>(World->GetWorldSettings());
|
||||
if (!Capture)
|
||||
{
|
||||
OutError = TEXT("failed to create SceneCaptureComponent2D");
|
||||
return false;
|
||||
}
|
||||
Capture->TextureTarget = RT;
|
||||
Capture->CaptureSource = ESceneCaptureSource::SCS_FinalColorLDR;
|
||||
Capture->FOVAngle = FMath::Clamp(FOV, 5.f, 170.f);
|
||||
Capture->bCaptureEveryFrame = false;
|
||||
Capture->bCaptureOnMovement = false;
|
||||
Capture->bAlwaysPersistRenderingState = true;
|
||||
Capture->RegisterComponentWithWorld(World);
|
||||
Capture->SetWorldLocationAndRotation(Location, Rotation);
|
||||
|
||||
Capture->CaptureScene();
|
||||
FlushRenderingCommands();
|
||||
|
||||
FTextureRenderTargetResource* RTResource = RT->GameThread_GetRenderTargetResource();
|
||||
bool bOk = false;
|
||||
if (RTResource)
|
||||
{
|
||||
TArray<FColor> Pixels;
|
||||
Pixels.SetNumUninitialized(W * H);
|
||||
FReadSurfaceDataFlags ReadFlags(RCM_UNorm);
|
||||
ReadFlags.SetLinearToGamma(false);
|
||||
if (RTResource->ReadPixels(Pixels, ReadFlags))
|
||||
{
|
||||
// FColor is BGRA in memory.
|
||||
bOk = SaveBGRAToPNG(
|
||||
reinterpret_cast<const uint8*>(Pixels.GetData()),
|
||||
Pixels.Num() * sizeof(FColor),
|
||||
W, H, OutputAbsPath, FileNameNoExt, OutFullPath, OutError);
|
||||
}
|
||||
else
|
||||
{
|
||||
OutError = TEXT("ReadPixels failed");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
OutError = TEXT("RT resource null after capture");
|
||||
}
|
||||
|
||||
// Tear down the transient component.
|
||||
Capture->UnregisterComponent();
|
||||
Capture->DestroyComponent();
|
||||
|
||||
return bOk;
|
||||
}
|
||||
2372
Source/UEBlueprintMCPEditor/Private/MCPGraphLibrary.cpp
Normal file
2372
Source/UEBlueprintMCPEditor/Private/MCPGraphLibrary.cpp
Normal file
File diff suppressed because it is too large
Load Diff
338
Source/UEBlueprintMCPEditor/Private/MCPMaterialLibrary.cpp
Normal file
338
Source/UEBlueprintMCPEditor/Private/MCPMaterialLibrary.cpp
Normal file
@ -0,0 +1,338 @@
|
||||
#include "MCPMaterialLibrary.h"
|
||||
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "Dom/JsonValue.h"
|
||||
#include "Materials/Material.h"
|
||||
#include "Materials/MaterialExpression.h"
|
||||
#include "Materials/MaterialFunction.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
FString JsonStringifyMaterial(const TSharedRef<FJsonObject>& Object)
|
||||
{
|
||||
FString Out;
|
||||
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Out);
|
||||
FJsonSerializer::Serialize(Object, Writer);
|
||||
return Out;
|
||||
}
|
||||
|
||||
FString ExpressionId(const UMaterialExpression* Expression)
|
||||
{
|
||||
if (!Expression)
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
if (Expression->MaterialExpressionGuid.IsValid())
|
||||
{
|
||||
return Expression->MaterialExpressionGuid.ToString(EGuidFormats::DigitsWithHyphensInBraces);
|
||||
}
|
||||
#endif
|
||||
|
||||
return Expression->GetPathName();
|
||||
}
|
||||
|
||||
FString OutputNameForIndex(UMaterialExpression* Expression, int32 OutputIndex)
|
||||
{
|
||||
if (!Expression || OutputIndex < 0)
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
TArray<FExpressionOutput>& Outputs = Expression->GetOutputs();
|
||||
if (!Outputs.IsValidIndex(OutputIndex))
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
return Outputs[OutputIndex].OutputName.ToString();
|
||||
}
|
||||
|
||||
int32 ResolveOutputIndex(UMaterialExpression* Expression, const FString& OutputNameOrIndex)
|
||||
{
|
||||
if (!Expression)
|
||||
{
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
FString Needle = OutputNameOrIndex;
|
||||
Needle.TrimStartAndEndInline();
|
||||
if (Needle.IsEmpty() || Needle.Equals(TEXT("None"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32 NumericIndex = INDEX_NONE;
|
||||
if (LexTryParseString(NumericIndex, *Needle))
|
||||
{
|
||||
return Expression->GetOutputs().IsValidIndex(NumericIndex) ? NumericIndex : INDEX_NONE;
|
||||
}
|
||||
|
||||
TArray<FExpressionOutput>& Outputs = Expression->GetOutputs();
|
||||
for (int32 OutputIndex = 0; OutputIndex < Outputs.Num(); ++OutputIndex)
|
||||
{
|
||||
if (Outputs[OutputIndex].OutputName.ToString().Equals(Needle, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return OutputIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return Outputs.Num() == 1 ? 0 : INDEX_NONE;
|
||||
}
|
||||
|
||||
int32 ResolveInputIndex(UMaterialExpression* Expression, const FString& InputNameOrIndex)
|
||||
{
|
||||
if (!Expression)
|
||||
{
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
FString Needle = InputNameOrIndex;
|
||||
Needle.TrimStartAndEndInline();
|
||||
if (Needle.IsEmpty() || Needle.Equals(TEXT("None"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
return Expression->CountInputs() == 1 ? 0 : INDEX_NONE;
|
||||
}
|
||||
|
||||
int32 NumericIndex = INDEX_NONE;
|
||||
if (LexTryParseString(NumericIndex, *Needle))
|
||||
{
|
||||
return NumericIndex >= 0 && NumericIndex < Expression->CountInputs() ? NumericIndex : INDEX_NONE;
|
||||
}
|
||||
|
||||
for (int32 InputIndex = 0; InputIndex < Expression->CountInputs(); ++InputIndex)
|
||||
{
|
||||
if (Expression->GetInputName(InputIndex).ToString().Equals(Needle, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return InputIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> MakeStatusJson(bool bOk, const FString& Error = FString())
|
||||
{
|
||||
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
||||
Root->SetBoolField(TEXT("ok"), bOk);
|
||||
if (!Error.IsEmpty())
|
||||
{
|
||||
Root->SetStringField(TEXT("error"), Error);
|
||||
}
|
||||
return Root;
|
||||
}
|
||||
|
||||
void AppendExpressionJson(UMaterialExpression* Expression, TArray<TSharedPtr<FJsonValue>>& Nodes, TArray<TSharedPtr<FJsonValue>>& Edges)
|
||||
{
|
||||
if (!Expression)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Node = MakeShared<FJsonObject>();
|
||||
Node->SetStringField(TEXT("guid"), ExpressionId(Expression));
|
||||
Node->SetStringField(TEXT("name"), Expression->GetName());
|
||||
Node->SetStringField(TEXT("class"), Expression->GetClass()->GetPathName());
|
||||
#if WITH_EDITORONLY_DATA
|
||||
Node->SetNumberField(TEXT("x"), Expression->MaterialExpressionEditorX);
|
||||
Node->SetNumberField(TEXT("y"), Expression->MaterialExpressionEditorY);
|
||||
if (!Expression->Desc.IsEmpty())
|
||||
{
|
||||
Node->SetStringField(TEXT("desc"), Expression->Desc);
|
||||
}
|
||||
#endif
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Inputs;
|
||||
const int32 InputCount = Expression->CountInputs();
|
||||
for (int32 InputIndex = 0; InputIndex < InputCount; ++InputIndex)
|
||||
{
|
||||
const FExpressionInput* Input = Expression->GetInput(InputIndex);
|
||||
const FName InputName = Expression->GetInputName(InputIndex);
|
||||
|
||||
TSharedRef<FJsonObject> InputJson = MakeShared<FJsonObject>();
|
||||
InputJson->SetStringField(TEXT("name"), InputName.ToString());
|
||||
InputJson->SetNumberField(TEXT("index"), InputIndex);
|
||||
InputJson->SetBoolField(TEXT("connected"), Input && Input->Expression);
|
||||
Inputs.Add(MakeShared<FJsonValueObject>(InputJson));
|
||||
|
||||
if (Input && Input->Expression)
|
||||
{
|
||||
TSharedRef<FJsonObject> Edge = MakeShared<FJsonObject>();
|
||||
TArray<TSharedPtr<FJsonValue>> From;
|
||||
From.Add(MakeShared<FJsonValueString>(ExpressionId(Input->Expression)));
|
||||
From.Add(MakeShared<FJsonValueString>(OutputNameForIndex(Input->Expression, Input->OutputIndex)));
|
||||
TArray<TSharedPtr<FJsonValue>> To;
|
||||
To.Add(MakeShared<FJsonValueString>(ExpressionId(Expression)));
|
||||
To.Add(MakeShared<FJsonValueString>(InputName.ToString()));
|
||||
Edge->SetArrayField(TEXT("from"), From);
|
||||
Edge->SetArrayField(TEXT("to"), To);
|
||||
Edges.Add(MakeShared<FJsonValueObject>(Edge));
|
||||
}
|
||||
}
|
||||
Node->SetArrayField(TEXT("inputs"), Inputs);
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> OutputsJson;
|
||||
TArray<FExpressionOutput>& Outputs = Expression->GetOutputs();
|
||||
for (int32 OutputIndex = 0; OutputIndex < Outputs.Num(); ++OutputIndex)
|
||||
{
|
||||
TSharedRef<FJsonObject> OutputJson = MakeShared<FJsonObject>();
|
||||
OutputJson->SetStringField(TEXT("name"), Outputs[OutputIndex].OutputName.ToString());
|
||||
OutputJson->SetNumberField(TEXT("index"), OutputIndex);
|
||||
OutputsJson.Add(MakeShared<FJsonValueObject>(OutputJson));
|
||||
}
|
||||
Node->SetArrayField(TEXT("outputs"), OutputsJson);
|
||||
|
||||
Nodes.Add(MakeShared<FJsonValueObject>(Node));
|
||||
}
|
||||
}
|
||||
|
||||
TArray<UMaterialExpression*> UMCPMaterialLibrary::GetMaterialExpressions(UObject* MaterialOrFunction)
|
||||
{
|
||||
TArray<UMaterialExpression*> Out;
|
||||
if (UMaterial* Material = Cast<UMaterial>(MaterialOrFunction))
|
||||
{
|
||||
for (TObjectPtr<UMaterialExpression> Expression : Material->GetExpressionCollection().Expressions)
|
||||
{
|
||||
if (Expression)
|
||||
{
|
||||
Out.Add(Expression.Get());
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (UMaterialFunction* Function = Cast<UMaterialFunction>(MaterialOrFunction))
|
||||
{
|
||||
for (TObjectPtr<UMaterialExpression> Expression : Function->GetExpressionCollection().Expressions)
|
||||
{
|
||||
if (Expression)
|
||||
{
|
||||
Out.Add(Expression.Get());
|
||||
}
|
||||
}
|
||||
}
|
||||
return Out;
|
||||
}
|
||||
|
||||
UMaterialExpression* UMCPMaterialLibrary::FindMaterialExpression(UObject* MaterialOrFunction, const FString& NodeId)
|
||||
{
|
||||
for (UMaterialExpression* Expression : GetMaterialExpressions(MaterialOrFunction))
|
||||
{
|
||||
if (!Expression)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ExpressionId(Expression) == NodeId || Expression->GetName() == NodeId || Expression->GetPathName() == NodeId)
|
||||
{
|
||||
return Expression;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FString UMCPMaterialLibrary::DumpMaterialGraphToJson(UObject* MaterialOrFunction)
|
||||
{
|
||||
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
||||
if (!MaterialOrFunction)
|
||||
{
|
||||
Root->SetBoolField(TEXT("ok"), false);
|
||||
Root->SetStringField(TEXT("error"), TEXT("null material/function"));
|
||||
return JsonStringifyMaterial(Root);
|
||||
}
|
||||
|
||||
Root->SetBoolField(TEXT("ok"), true);
|
||||
Root->SetStringField(TEXT("asset"), MaterialOrFunction->GetPathName());
|
||||
Root->SetStringField(TEXT("class"), MaterialOrFunction->GetClass()->GetPathName());
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Nodes;
|
||||
TArray<TSharedPtr<FJsonValue>> Edges;
|
||||
for (UMaterialExpression* Expression : GetMaterialExpressions(MaterialOrFunction))
|
||||
{
|
||||
AppendExpressionJson(Expression, Nodes, Edges);
|
||||
}
|
||||
Root->SetArrayField(TEXT("nodes"), Nodes);
|
||||
Root->SetArrayField(TEXT("edges"), Edges);
|
||||
|
||||
if (UMaterial* Material = Cast<UMaterial>(MaterialOrFunction))
|
||||
{
|
||||
TSharedRef<FJsonObject> Outputs = MakeShared<FJsonObject>();
|
||||
for (int32 PropertyIndex = 0; PropertyIndex < static_cast<int32>(MP_MAX); ++PropertyIndex)
|
||||
{
|
||||
const EMaterialProperty Property = static_cast<EMaterialProperty>(PropertyIndex);
|
||||
FExpressionInput* Input = Material->GetExpressionInputForProperty(Property);
|
||||
if (!Input || !Input->Expression)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Ref = MakeShared<FJsonObject>();
|
||||
TArray<TSharedPtr<FJsonValue>> From;
|
||||
From.Add(MakeShared<FJsonValueString>(ExpressionId(Input->Expression)));
|
||||
From.Add(MakeShared<FJsonValueString>(OutputNameForIndex(Input->Expression, Input->OutputIndex)));
|
||||
Ref->SetArrayField(TEXT("from"), From);
|
||||
Outputs->SetObjectField(UEnum::GetValueAsString(Property), Ref);
|
||||
}
|
||||
Root->SetObjectField(TEXT("outputs"), Outputs);
|
||||
}
|
||||
|
||||
return JsonStringifyMaterial(Root);
|
||||
}
|
||||
|
||||
FString UMCPMaterialLibrary::ConnectMaterialExpressionsRaw(UObject* MaterialOrFunction, UMaterialExpression* FromExpression, const FString& FromOutput, UMaterialExpression* ToExpression, const FString& ToInput)
|
||||
{
|
||||
if (!MaterialOrFunction)
|
||||
{
|
||||
return JsonStringifyMaterial(MakeStatusJson(false, TEXT("null material/function")));
|
||||
}
|
||||
if (!FromExpression)
|
||||
{
|
||||
return JsonStringifyMaterial(MakeStatusJson(false, TEXT("null source expression")));
|
||||
}
|
||||
if (!ToExpression)
|
||||
{
|
||||
return JsonStringifyMaterial(MakeStatusJson(false, TEXT("null target expression")));
|
||||
}
|
||||
|
||||
const int32 OutputIndex = ResolveOutputIndex(FromExpression, FromOutput);
|
||||
if (OutputIndex == INDEX_NONE)
|
||||
{
|
||||
return JsonStringifyMaterial(MakeStatusJson(false, FString::Printf(TEXT("source output not found: %s.%s"), *FromExpression->GetName(), *FromOutput)));
|
||||
}
|
||||
|
||||
const int32 InputIndex = ResolveInputIndex(ToExpression, ToInput);
|
||||
if (InputIndex == INDEX_NONE)
|
||||
{
|
||||
return JsonStringifyMaterial(MakeStatusJson(false, FString::Printf(TEXT("target input not found: %s.%s"), *ToExpression->GetName(), *ToInput)));
|
||||
}
|
||||
|
||||
FExpressionInput* Input = ToExpression->GetInput(InputIndex);
|
||||
if (!Input)
|
||||
{
|
||||
return JsonStringifyMaterial(MakeStatusJson(false, FString::Printf(TEXT("target input is null: %s.%s"), *ToExpression->GetName(), *ToInput)));
|
||||
}
|
||||
|
||||
Input->Expression = FromExpression;
|
||||
Input->OutputIndex = OutputIndex;
|
||||
|
||||
if (UMaterial* Material = Cast<UMaterial>(MaterialOrFunction))
|
||||
{
|
||||
Material->PreEditChange(nullptr);
|
||||
Material->PostEditChange();
|
||||
Material->MarkPackageDirty();
|
||||
}
|
||||
else if (UMaterialFunction* Function = Cast<UMaterialFunction>(MaterialOrFunction))
|
||||
{
|
||||
Function->PreEditChange(nullptr);
|
||||
Function->PostEditChange();
|
||||
Function->MarkPackageDirty();
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Root = MakeStatusJson(true);
|
||||
Root->SetStringField(TEXT("from"), FromExpression->GetName());
|
||||
Root->SetStringField(TEXT("from_output"), OutputNameForIndex(FromExpression, OutputIndex));
|
||||
Root->SetStringField(TEXT("to"), ToExpression->GetName());
|
||||
Root->SetStringField(TEXT("to_input"), ToExpression->GetInputName(InputIndex).ToString());
|
||||
return JsonStringifyMaterial(Root);
|
||||
}
|
||||
275
Source/UEBlueprintMCPEditor/Private/MCPNiagaraLibrary.cpp
Normal file
275
Source/UEBlueprintMCPEditor/Private/MCPNiagaraLibrary.cpp
Normal file
@ -0,0 +1,275 @@
|
||||
#include "MCPNiagaraLibrary.h"
|
||||
|
||||
#include "AssetToolsModule.h"
|
||||
#include "IAssetTools.h"
|
||||
#include "AssetRegistry/AssetData.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "UObject/Package.h"
|
||||
#include "UObject/UObjectGlobals.h"
|
||||
|
||||
#include "NiagaraSystem.h"
|
||||
#include "NiagaraSystemFactoryNew.h"
|
||||
#include "NiagaraEmitter.h"
|
||||
#include "NiagaraEmitterHandle.h"
|
||||
#include "NiagaraScript.h"
|
||||
#include "NiagaraRendererProperties.h"
|
||||
#include "NiagaraScriptSource.h"
|
||||
#include "NiagaraGraph.h"
|
||||
#include "NiagaraNodeOutput.h"
|
||||
#include "NiagaraNodeFunctionCall.h"
|
||||
#include "NiagaraCommon.h"
|
||||
#include "ViewModels/Stack/NiagaraStackGraphUtilities.h"
|
||||
#include "NiagaraEditorUtilities.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
struct FStackLocation
|
||||
{
|
||||
UNiagaraNodeOutput* OutputNode = nullptr;
|
||||
bool bValid() const { return OutputNode != nullptr; }
|
||||
};
|
||||
|
||||
ENiagaraScriptUsage ResolveUsage(const FString& GroupName, bool& bOutIsSystem)
|
||||
{
|
||||
bOutIsSystem = false;
|
||||
const FString G = GroupName.TrimStartAndEnd();
|
||||
if (G.Equals(TEXT("EmitterSpawn"), ESearchCase::IgnoreCase)) return ENiagaraScriptUsage::EmitterSpawnScript;
|
||||
if (G.Equals(TEXT("EmitterUpdate"), ESearchCase::IgnoreCase)) return ENiagaraScriptUsage::EmitterUpdateScript;
|
||||
if (G.Equals(TEXT("ParticleSpawn"), ESearchCase::IgnoreCase)) return ENiagaraScriptUsage::ParticleSpawnScript;
|
||||
if (G.Equals(TEXT("ParticleUpdate"), ESearchCase::IgnoreCase)) return ENiagaraScriptUsage::ParticleUpdateScript;
|
||||
if (G.Equals(TEXT("SystemSpawn"), ESearchCase::IgnoreCase)) { bOutIsSystem = true; return ENiagaraScriptUsage::SystemSpawnScript; }
|
||||
if (G.Equals(TEXT("SystemUpdate"), ESearchCase::IgnoreCase)) { bOutIsSystem = true; return ENiagaraScriptUsage::SystemUpdateScript; }
|
||||
return ENiagaraScriptUsage::ParticleSpawnScript;
|
||||
}
|
||||
|
||||
UNiagaraNodeOutput* FindOutputNodeFromScript(UNiagaraScript* Script, ENiagaraScriptUsage Usage)
|
||||
{
|
||||
if (!Script) return nullptr;
|
||||
UNiagaraScriptSourceBase* SourceBase = Script->GetLatestSource();
|
||||
UNiagaraScriptSource* Source = Cast<UNiagaraScriptSource>(SourceBase);
|
||||
if (!Source || !Source->NodeGraph) return nullptr;
|
||||
// FindOutputNode is not NIAGARAEDITOR_API; FindEquivalentOutputNode is.
|
||||
return Source->NodeGraph->FindEquivalentOutputNode(Usage, FGuid());
|
||||
}
|
||||
|
||||
void GatherFunctionCallNodes(UNiagaraNodeOutput* OutputNode, TArray<UNiagaraNodeFunctionCall*>& Out)
|
||||
{
|
||||
if (!OutputNode) return;
|
||||
UEdGraph* Graph = OutputNode->GetGraph();
|
||||
if (!Graph) return;
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (UNiagaraNodeFunctionCall* FC = Cast<UNiagaraNodeFunctionCall>(Node))
|
||||
{
|
||||
// Filter to modules attached to this output's usage. GetCalledUsage()
|
||||
// returns the usage of the inner module script (usually Module).
|
||||
// Without param-map walking we list all function-calls in the graph
|
||||
// tied to the same output usage — good enough for list/remove.
|
||||
Out.Add(FC);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FNiagaraEmitterHandle* FindEmitterHandleMutable(UNiagaraSystem* System, const FString& EmitterName)
|
||||
{
|
||||
if (!System) return nullptr;
|
||||
TArray<FNiagaraEmitterHandle>& Handles = System->GetEmitterHandles();
|
||||
for (FNiagaraEmitterHandle& H : Handles)
|
||||
{
|
||||
if (H.GetName().ToString().Equals(EmitterName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return &H;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FStackLocation LocateStack(UNiagaraSystem* System, const FString& EmitterName, const FString& GroupName)
|
||||
{
|
||||
FStackLocation Loc;
|
||||
bool bIsSystem = false;
|
||||
const ENiagaraScriptUsage Usage = ResolveUsage(GroupName, bIsSystem);
|
||||
|
||||
if (!System) return Loc;
|
||||
|
||||
if (bIsSystem)
|
||||
{
|
||||
UNiagaraScript* Script = (Usage == ENiagaraScriptUsage::SystemSpawnScript)
|
||||
? System->GetSystemSpawnScript()
|
||||
: System->GetSystemUpdateScript();
|
||||
Loc.OutputNode = FindOutputNodeFromScript(Script, Usage);
|
||||
return Loc;
|
||||
}
|
||||
|
||||
FNiagaraEmitterHandle* Handle = FindEmitterHandleMutable(System, EmitterName);
|
||||
if (!Handle) return Loc;
|
||||
FVersionedNiagaraEmitter Versioned = Handle->GetInstance();
|
||||
FVersionedNiagaraEmitterData* Data = Versioned.GetEmitterData();
|
||||
if (!Data) return Loc;
|
||||
UNiagaraScript* Script = Data->GetScript(Usage, FGuid());
|
||||
Loc.OutputNode = FindOutputNodeFromScript(Script, Usage);
|
||||
return Loc;
|
||||
}
|
||||
}
|
||||
|
||||
bool UMCPNiagaraLibrary::CreateNiagaraSystem(const FString& PackagePath)
|
||||
{
|
||||
if (PackagePath.IsEmpty()) return false;
|
||||
|
||||
FString PackageName, AssetName;
|
||||
if (!PackagePath.Split(TEXT("/"), &PackageName, &AssetName, ESearchCase::IgnoreCase, ESearchDir::FromEnd))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
|
||||
UNiagaraSystemFactoryNew* Factory = NewObject<UNiagaraSystemFactoryNew>();
|
||||
UObject* Created = AssetTools.CreateAsset(AssetName, PackageName, UNiagaraSystem::StaticClass(), Factory);
|
||||
return Created != nullptr;
|
||||
}
|
||||
|
||||
TArray<FString> UMCPNiagaraLibrary::GetEmitterHandleNames(UNiagaraSystem* System)
|
||||
{
|
||||
TArray<FString> Out;
|
||||
if (!System) return Out;
|
||||
for (const FNiagaraEmitterHandle& H : System->GetEmitterHandles())
|
||||
{
|
||||
Out.Add(H.GetName().ToString());
|
||||
}
|
||||
return Out;
|
||||
}
|
||||
|
||||
FString UMCPNiagaraLibrary::AddEmitterToSystem(UNiagaraSystem* System, UNiagaraEmitter* SourceEmitter, const FString& EmitterName)
|
||||
{
|
||||
if (!System || !SourceEmitter) return FString();
|
||||
|
||||
System->Modify();
|
||||
// Use the editor-utility path that performs full setup (merge, renderer
|
||||
// initialization, parameter refresh). The raw UNiagaraSystem::AddEmitterHandle
|
||||
// skips most of this and leaves the emitter non-functional in preview.
|
||||
const FGuid HandleId = FNiagaraEditorUtilities::AddEmitterToSystem(
|
||||
*System, *SourceEmitter, FGuid(), /*bCreateCopy=*/true);
|
||||
if (!HandleId.IsValid()) return FString();
|
||||
|
||||
// Find the handle we just added by id and (optionally) rename it.
|
||||
for (FNiagaraEmitterHandle& H : System->GetEmitterHandles())
|
||||
{
|
||||
if (H.GetId() == HandleId)
|
||||
{
|
||||
if (!EmitterName.IsEmpty())
|
||||
{
|
||||
H.SetName(FName(*EmitterName), *System);
|
||||
}
|
||||
return H.GetName().ToString();
|
||||
}
|
||||
}
|
||||
return FString();
|
||||
}
|
||||
|
||||
bool UMCPNiagaraLibrary::RemoveEmitterFromSystem(UNiagaraSystem* System, const FString& EmitterName)
|
||||
{
|
||||
if (!System) return false;
|
||||
FNiagaraEmitterHandle* Handle = FindEmitterHandleMutable(System, EmitterName);
|
||||
if (!Handle) return false;
|
||||
System->Modify();
|
||||
System->RemoveEmitterHandle(*Handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
TArray<FString> UMCPNiagaraLibrary::ListModules(UNiagaraSystem* System, const FString& EmitterName, const FString& GroupName)
|
||||
{
|
||||
TArray<FString> Out;
|
||||
const FStackLocation Loc = LocateStack(System, EmitterName, GroupName);
|
||||
if (!Loc.bValid()) return Out;
|
||||
|
||||
TArray<UNiagaraNodeFunctionCall*> ModuleNodes;
|
||||
GatherFunctionCallNodes(Loc.OutputNode, ModuleNodes);
|
||||
for (UNiagaraNodeFunctionCall* N : ModuleNodes)
|
||||
{
|
||||
if (N) Out.Add(N->GetFunctionName());
|
||||
}
|
||||
return Out;
|
||||
}
|
||||
|
||||
FString UMCPNiagaraLibrary::AddModuleToStack(UNiagaraSystem* System, const FString& EmitterName, const FString& GroupName, UNiagaraScript* ModuleScript, int32 Index)
|
||||
{
|
||||
if (!ModuleScript) return FString();
|
||||
const FStackLocation Loc = LocateStack(System, EmitterName, GroupName);
|
||||
if (!Loc.bValid()) return FString();
|
||||
|
||||
System->Modify();
|
||||
UNiagaraNodeFunctionCall* Added = FNiagaraStackGraphUtilities::AddScriptModuleToStack(
|
||||
ModuleScript, *Loc.OutputNode, Index, FString(), FGuid());
|
||||
if (!Added) return FString();
|
||||
return Added->GetFunctionName();
|
||||
}
|
||||
|
||||
bool UMCPNiagaraLibrary::RemoveModuleFromStack(UNiagaraSystem* System, const FString& EmitterName, const FString& GroupName, const FString& ModuleName)
|
||||
{
|
||||
const FStackLocation Loc = LocateStack(System, EmitterName, GroupName);
|
||||
if (!Loc.bValid()) return false;
|
||||
|
||||
TArray<UNiagaraNodeFunctionCall*> ModuleNodes;
|
||||
GatherFunctionCallNodes(Loc.OutputNode, ModuleNodes);
|
||||
|
||||
for (UNiagaraNodeFunctionCall* N : ModuleNodes)
|
||||
{
|
||||
if (N && N->GetFunctionName().Equals(ModuleName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
System->Modify();
|
||||
UEdGraph* Graph = N->GetGraph();
|
||||
if (Graph)
|
||||
{
|
||||
Graph->Modify();
|
||||
N->BreakAllNodeLinks();
|
||||
Graph->RemoveNode(N);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FString UMCPNiagaraLibrary::AddRendererToEmitter(UNiagaraSystem* System, const FString& EmitterName, UClass* RendererClass)
|
||||
{
|
||||
if (!RendererClass || !RendererClass->IsChildOf(UNiagaraRendererProperties::StaticClass())) return FString();
|
||||
FNiagaraEmitterHandle* Handle = FindEmitterHandleMutable(System, EmitterName);
|
||||
if (!Handle) return FString();
|
||||
FVersionedNiagaraEmitter Versioned = Handle->GetInstance();
|
||||
UNiagaraEmitter* Emitter = Versioned.Emitter;
|
||||
if (!Emitter) return FString();
|
||||
|
||||
Emitter->Modify();
|
||||
UNiagaraRendererProperties* Renderer = NewObject<UNiagaraRendererProperties>(Emitter, RendererClass, NAME_None, RF_Transactional);
|
||||
Emitter->AddRenderer(Renderer, Versioned.Version);
|
||||
return RendererClass->GetName();
|
||||
}
|
||||
|
||||
int32 UMCPNiagaraLibrary::RemoveRenderersFromEmitter(UNiagaraSystem* System, const FString& EmitterName, UClass* RendererClass)
|
||||
{
|
||||
if (!RendererClass) return 0;
|
||||
FNiagaraEmitterHandle* Handle = FindEmitterHandleMutable(System, EmitterName);
|
||||
if (!Handle) return 0;
|
||||
FVersionedNiagaraEmitter Versioned = Handle->GetInstance();
|
||||
UNiagaraEmitter* Emitter = Versioned.Emitter;
|
||||
FVersionedNiagaraEmitterData* Data = Versioned.GetEmitterData();
|
||||
if (!Emitter || !Data) return 0;
|
||||
|
||||
Emitter->Modify();
|
||||
TArray<UNiagaraRendererProperties*> ToRemove;
|
||||
for (UNiagaraRendererProperties* R : Data->GetRenderers())
|
||||
{
|
||||
if (R && R->IsA(RendererClass)) ToRemove.Add(R);
|
||||
}
|
||||
for (UNiagaraRendererProperties* R : ToRemove)
|
||||
{
|
||||
Emitter->RemoveRenderer(R, Versioned.Version);
|
||||
}
|
||||
return ToRemove.Num();
|
||||
}
|
||||
|
||||
bool UMCPNiagaraLibrary::RequestCompile(UNiagaraSystem* System)
|
||||
{
|
||||
if (!System) return false;
|
||||
return System->RequestCompile(true);
|
||||
}
|
||||
948
Source/UEBlueprintMCPEditor/Private/MCPVoxelGraphLibrary.cpp
Normal file
948
Source/UEBlueprintMCPEditor/Private/MCPVoxelGraphLibrary.cpp
Normal file
@ -0,0 +1,948 @@
|
||||
#include "MCPVoxelGraphLibrary.h"
|
||||
|
||||
#include "MCPGraphLibrary.h"
|
||||
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphNode.h"
|
||||
#include "EdGraph/EdGraphPin.h"
|
||||
#include "EdGraphNode_Comment.h"
|
||||
#include "Subsystems/AssetEditorSubsystem.h"
|
||||
#include "Editor.h"
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "Dom/JsonValue.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
|
||||
// WITH_MCP_VOXEL is set by UEBlueprintMCPEditor.Build.cs: 1 when the Voxel plugin
|
||||
// is present (auto-detected, or forced via env UE_MCP_WITH_VOXEL), 0 otherwise.
|
||||
// When 0 the whole Voxel integration compiles down to harmless stubs so the
|
||||
// plugin builds in any project — with or without Voxel installed.
|
||||
#ifndef WITH_MCP_VOXEL
|
||||
#define WITH_MCP_VOXEL 0
|
||||
#endif
|
||||
|
||||
#if WITH_MCP_VOXEL
|
||||
|
||||
#include "VoxelGraph.h"
|
||||
#include "VoxelGraphToolkit.h"
|
||||
#include "VoxelGraphTracker.h"
|
||||
#include "VoxelTerminalGraph.h"
|
||||
#include "VoxelTerminalGraphRuntime.h"
|
||||
|
||||
#include "VoxelMinimal.h"
|
||||
#include "VoxelNode.h"
|
||||
#include "VoxelFunctionLibrary.h"
|
||||
#include "Nodes/VoxelNode_UFunction.h"
|
||||
#include "VoxelMinimal/VoxelInstancedStruct.h"
|
||||
#include "VoxelMinimal/VoxelObjectHelpers.h"
|
||||
#include "VoxelParameter.h"
|
||||
#include "VoxelPinType.h"
|
||||
|
||||
// Private headers of the VoxelGraphEditor module (reachable via PrivateIncludePaths in Build.cs).
|
||||
// We only use inline/virtual members from these, so no cross-module link symbols are required.
|
||||
#include "Nodes/VoxelGraphNode_Struct.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
static FString VoxelJsonStringify(const TSharedRef<FJsonObject>& Object)
|
||||
{
|
||||
FString Out;
|
||||
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Out);
|
||||
FJsonSerializer::Serialize(Object, Writer);
|
||||
return Out;
|
||||
}
|
||||
|
||||
static UVoxelGraph* AsVoxelGraph(UObject* Asset)
|
||||
{
|
||||
return Cast<UVoxelGraph>(Asset);
|
||||
}
|
||||
|
||||
static void FillTerminalJson(TSharedRef<FJsonObject> Json, const UVoxelTerminalGraph& TerminalGraph)
|
||||
{
|
||||
Json->SetStringField(TEXT("guid"), TerminalGraph.GetGuid().ToString());
|
||||
Json->SetStringField(TEXT("name"), TerminalGraph.GetDisplayName());
|
||||
Json->SetBoolField(TEXT("is_main"), TerminalGraph.IsMainTerminalGraph());
|
||||
Json->SetBoolField(TEXT("is_editor"), TerminalGraph.IsEditorTerminalGraph());
|
||||
|
||||
const UEdGraph& EdGraph = TerminalGraph.GetEdGraph();
|
||||
Json->SetStringField(TEXT("ed_graph"), EdGraph.GetName());
|
||||
Json->SetNumberField(TEXT("node_count"), EdGraph.Nodes.Num());
|
||||
}
|
||||
|
||||
static UVoxelTerminalGraph* FindTerminalGraph(UVoxelGraph* Graph, const FString& Terminal)
|
||||
{
|
||||
if (!Graph)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (Terminal.IsEmpty() ||
|
||||
Terminal.Equals(TEXT("main"), ESearchCase::IgnoreCase) ||
|
||||
Terminal.Equals(TEXT("Main"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
return &Graph->GetMainTerminalGraph();
|
||||
}
|
||||
|
||||
if (Terminal.Equals(TEXT("editor"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
return &Graph->GetEditorTerminalGraph();
|
||||
}
|
||||
|
||||
FGuid Guid;
|
||||
if (FGuid::Parse(Terminal, Guid))
|
||||
{
|
||||
return Graph->FindTerminalGraph(Guid);
|
||||
}
|
||||
|
||||
UVoxelTerminalGraph* Result = nullptr;
|
||||
Graph->ForeachTerminalGraph_NoInheritance([&](UVoxelTerminalGraph& Candidate)
|
||||
{
|
||||
if (!Result &&
|
||||
(Candidate.GetDisplayName().Equals(Terminal, ESearchCase::IgnoreCase) ||
|
||||
Candidate.GetEdGraph().GetName().Equals(Terminal, ESearchCase::IgnoreCase)))
|
||||
{
|
||||
Result = &Candidate;
|
||||
}
|
||||
});
|
||||
return Result;
|
||||
}
|
||||
|
||||
static TSharedRef<FJsonObject> VoxelNodeToJson(UEdGraphNode* Node)
|
||||
{
|
||||
TSharedRef<FJsonObject> Json = MakeShared<FJsonObject>();
|
||||
Json->SetStringField(TEXT("guid"), Node->NodeGuid.ToString());
|
||||
Json->SetStringField(TEXT("class"), Node->GetClass()->GetName());
|
||||
Json->SetNumberField(TEXT("x"), Node->NodePosX);
|
||||
Json->SetNumberField(TEXT("y"), Node->NodePosY);
|
||||
Json->SetStringField(TEXT("title"), Node->GetNodeTitle(ENodeTitleType::ListView).ToString());
|
||||
if (!Node->NodeComment.IsEmpty())
|
||||
{
|
||||
Json->SetStringField(TEXT("comment"), Node->NodeComment);
|
||||
}
|
||||
if (const UEdGraphNode_Comment* CommentNode = Cast<UEdGraphNode_Comment>(Node))
|
||||
{
|
||||
Json->SetNumberField(TEXT("w"), CommentNode->NodeWidth);
|
||||
Json->SetNumberField(TEXT("h"), CommentNode->NodeHeight);
|
||||
}
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Pins;
|
||||
for (UEdGraphPin* Pin : Node->Pins)
|
||||
{
|
||||
if (!Pin)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> PinJson = MakeShared<FJsonObject>();
|
||||
PinJson->SetStringField(TEXT("name"), Pin->PinName.ToString());
|
||||
PinJson->SetStringField(TEXT("dir"), Pin->Direction == EGPD_Input ? TEXT("in") : TEXT("out"));
|
||||
PinJson->SetStringField(TEXT("type"), Pin->PinType.PinCategory.ToString());
|
||||
if (!Pin->PinType.PinSubCategory.IsNone())
|
||||
{
|
||||
PinJson->SetStringField(TEXT("subtype"), Pin->PinType.PinSubCategory.ToString());
|
||||
}
|
||||
if (Pin->PinType.PinSubCategoryObject.IsValid())
|
||||
{
|
||||
PinJson->SetStringField(TEXT("subobj"), Pin->PinType.PinSubCategoryObject->GetPathName());
|
||||
}
|
||||
if (!Pin->DefaultValue.IsEmpty())
|
||||
{
|
||||
PinJson->SetStringField(TEXT("default"), Pin->DefaultValue);
|
||||
}
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Links;
|
||||
for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
|
||||
{
|
||||
if (!LinkedPin || !LinkedPin->GetOwningNode())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> LinkJson = MakeShared<FJsonObject>();
|
||||
LinkJson->SetStringField(TEXT("node"), LinkedPin->GetOwningNode()->NodeGuid.ToString());
|
||||
LinkJson->SetStringField(TEXT("pin"), LinkedPin->PinName.ToString());
|
||||
Links.Add(MakeShared<FJsonValueObject>(LinkJson));
|
||||
}
|
||||
if (Links.Num() > 0)
|
||||
{
|
||||
PinJson->SetArrayField(TEXT("links"), Links);
|
||||
}
|
||||
Pins.Add(MakeShared<FJsonValueObject>(PinJson));
|
||||
}
|
||||
Json->SetArrayField(TEXT("pins"), Pins);
|
||||
return Json;
|
||||
}
|
||||
|
||||
// Lazily-built catalogue of every spawnable Voxel node. Mirrors FVoxelNodeLibrary's
|
||||
// construction (which lives in VoxelGraphEditor/Private and is not exported), so we
|
||||
// rebuild it here from the exported reflection helpers.
|
||||
static const TArray<TSharedRef<const FVoxelNode>>& GetVoxelNodeCatalogue()
|
||||
{
|
||||
static TArray<TSharedRef<const FVoxelNode>> Nodes;
|
||||
static bool bBuilt = false;
|
||||
if (bBuilt)
|
||||
{
|
||||
return Nodes;
|
||||
}
|
||||
bBuilt = true;
|
||||
|
||||
// Struct nodes (FVoxelNode subclasses): Add, Multiply, noise, output nodes, etc.
|
||||
for (UScriptStruct* Struct : GetDerivedStructs<FVoxelNode>())
|
||||
{
|
||||
if (!Struct)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (Struct->HasMetaData(TEXT("Abstract")) || Struct->HasMetaData(TEXT("Internal")))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Nodes.Add(MakeSharedStruct<FVoxelNode>(Struct));
|
||||
}
|
||||
|
||||
// Function-library nodes (UVoxelFunctionLibrary UFUNCTIONs wrapped as FVoxelNode_UFunction):
|
||||
// most math / position / curve nodes live here.
|
||||
for (const TSubclassOf<UVoxelFunctionLibrary>& Class : GetDerivedClasses<UVoxelFunctionLibrary>())
|
||||
{
|
||||
if (!Class)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (UFunction* Function : GetClassFunctions(Class))
|
||||
{
|
||||
if (!Function || Function->HasMetaData(TEXT("Internal")))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Nodes.Add(FVoxelNode_UFunction::Make(Function));
|
||||
}
|
||||
}
|
||||
|
||||
return Nodes;
|
||||
}
|
||||
|
||||
// The UFunction backing a function-library node, or nullptr for plain struct nodes.
|
||||
static UFunction* GetNodeFunction(const FVoxelNode& Node)
|
||||
{
|
||||
if (Node.GetStruct() &&
|
||||
Node.GetStruct()->IsChildOf(FVoxelNode_UFunction::StaticStruct()))
|
||||
{
|
||||
return static_cast<const FVoxelNode_UFunction&>(Node).GetFunction();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Primary, stable identifier used to spawn a node.
|
||||
static FString GetNodeId(const FVoxelNode& Node)
|
||||
{
|
||||
if (const UFunction* Function = GetNodeFunction(Node))
|
||||
{
|
||||
const UClass* Owner = Function->GetOwnerClass();
|
||||
return FString::Printf(TEXT("%s.%s"),
|
||||
Owner ? *Owner->GetName() : TEXT("?"),
|
||||
*Function->GetName());
|
||||
}
|
||||
return Node.GetStruct() ? Node.GetStruct()->GetName() : FString();
|
||||
}
|
||||
|
||||
// Returns true if NodeId matches the node by any of its accepted aliases.
|
||||
static bool NodeMatchesId(const FVoxelNode& Node, const FString& NodeId)
|
||||
{
|
||||
if (GetNodeId(Node).Equals(NodeId, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (Node.GetDisplayName().Equals(NodeId, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (const UFunction* Function = GetNodeFunction(Node))
|
||||
{
|
||||
if (Function->GetName().Equals(NodeId, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
const UClass* Owner = Function->GetOwnerClass();
|
||||
if (Owner &&
|
||||
FString::Printf(TEXT("%s::%s"), *Owner->GetName(), *Function->GetName()).Equals(NodeId, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (const UScriptStruct* Struct = Node.GetStruct())
|
||||
{
|
||||
if (Struct->GetPathName().Equals(NodeId, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static TSharedPtr<const FVoxelNode> FindCatalogueNode(const FString& NodeId)
|
||||
{
|
||||
for (const TSharedRef<const FVoxelNode>& Node : GetVoxelNodeCatalogue())
|
||||
{
|
||||
if (NodeMatchesId(*Node, NodeId))
|
||||
{
|
||||
return Node;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Map a friendly string to a Voxel pin type. Defaults to float for unknown input.
|
||||
static FVoxelPinType PinTypeFromString(const FString& TypeStr)
|
||||
{
|
||||
const FString T = TypeStr.ToLower();
|
||||
if (T == TEXT("double")) return FVoxelPinType::Make<double>();
|
||||
if (T == TEXT("int") || T == TEXT("int32")) return FVoxelPinType::Make<int32>();
|
||||
if (T == TEXT("bool")) return FVoxelPinType::Make<bool>();
|
||||
if (T == TEXT("vector2d") || T == TEXT("vector2"))return FVoxelPinType::Make<FVector2D>();
|
||||
if (T == TEXT("vector") || T == TEXT("vector3d")) return FVoxelPinType::Make<FVector>();
|
||||
if (T == TEXT("color") || T == TEXT("linearcolor"))return FVoxelPinType::Make<FLinearColor>();
|
||||
if (T == TEXT("name")) return FVoxelPinType::Make<FName>();
|
||||
return FVoxelPinType::Make<float>();
|
||||
}
|
||||
|
||||
// Reflection-based spawn of a (private, non-exported) Voxel graph node class by its script path,
|
||||
// setting its FGuid "Guid" UPROPERTY. Only inline/virtual calls -> no cross-module link symbols.
|
||||
static UEdGraphNode* SpawnGuidNodeReflect(UEdGraph* Graph, const TCHAR* ClassPath, const FGuid& Guid, FVector2D Position)
|
||||
{
|
||||
UClass* NodeClass = FindObject<UClass>(nullptr, ClassPath);
|
||||
if (!Graph || !NodeClass)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Graph->Modify();
|
||||
UEdGraphNode* Node = NewObject<UEdGraphNode>(Graph, NodeClass, NAME_None, RF_Transactional);
|
||||
if (!Node)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (FStructProperty* GuidProp = FindFProperty<FStructProperty>(NodeClass, TEXT("Guid")))
|
||||
{
|
||||
*GuidProp->ContainerPtrToValuePtr<FGuid>(Node) = Guid;
|
||||
}
|
||||
|
||||
Node->NodePosX = static_cast<int32>(Position.X);
|
||||
Node->NodePosY = static_cast<int32>(Position.Y);
|
||||
Node->CreateNewGuid();
|
||||
Graph->AddNode(Node, true, false);
|
||||
Node->PostPlacedNewNode();
|
||||
if (Node->Pins.Num() == 0)
|
||||
{
|
||||
Node->AllocateDefaultPins();
|
||||
}
|
||||
return Node;
|
||||
}
|
||||
|
||||
static UVoxelTerminalGraph* OwningTerminal(UEdGraph* Graph)
|
||||
{
|
||||
return Graph ? Graph->GetTypedOuter<UVoxelTerminalGraph>() : nullptr;
|
||||
}
|
||||
|
||||
static FGuid ResolveParameterGuid(UVoxelGraph* Graph, const FString& Ref)
|
||||
{
|
||||
if (!Graph)
|
||||
{
|
||||
return FGuid();
|
||||
}
|
||||
FGuid Guid;
|
||||
if (FGuid::Parse(Ref, Guid) && Graph->FindParameter(Guid))
|
||||
{
|
||||
return Guid;
|
||||
}
|
||||
for (const FGuid& Candidate : Graph->GetParameters())
|
||||
{
|
||||
if (Graph->FindParameterChecked(Candidate).Name.ToString().Equals(Ref, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return Candidate;
|
||||
}
|
||||
}
|
||||
return FGuid();
|
||||
}
|
||||
|
||||
// Resolve a terminal-graph property (kind: input/output/local) by guid or name.
|
||||
static FGuid ResolveTerminalPropGuid(UVoxelTerminalGraph* Terminal, const FString& Kind, const FString& Ref)
|
||||
{
|
||||
if (!Terminal)
|
||||
{
|
||||
return FGuid();
|
||||
}
|
||||
FGuid Guid;
|
||||
const bool bIsGuid = FGuid::Parse(Ref, Guid);
|
||||
|
||||
if (Kind == TEXT("input"))
|
||||
{
|
||||
if (bIsGuid && Terminal->FindInput(Guid)) return Guid;
|
||||
for (const FGuid& G : Terminal->GetFunctionInputs())
|
||||
if (Terminal->FindInputChecked(G).Name.ToString().Equals(Ref, ESearchCase::IgnoreCase)) return G;
|
||||
}
|
||||
else if (Kind == TEXT("output"))
|
||||
{
|
||||
if (bIsGuid && Terminal->FindOutput(Guid)) return Guid;
|
||||
for (const FGuid& G : Terminal->GetFunctionOutputs())
|
||||
if (Terminal->FindOutputChecked(G).Name.ToString().Equals(Ref, ESearchCase::IgnoreCase)) return G;
|
||||
}
|
||||
else if (Kind == TEXT("local"))
|
||||
{
|
||||
if (bIsGuid && Terminal->FindLocalVariable(Guid)) return Guid;
|
||||
for (const FGuid& G : Terminal->GetLocalVariables())
|
||||
if (Terminal->FindLocalVariableChecked(G).Name.ToString().Equals(Ref, ESearchCase::IgnoreCase)) return G;
|
||||
}
|
||||
return FGuid();
|
||||
}
|
||||
|
||||
static TSharedRef<FJsonObject> JsonError(const FString& Message)
|
||||
{
|
||||
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
||||
Root->SetBoolField(TEXT("ok"), false);
|
||||
Root->SetStringField(TEXT("error"), Message);
|
||||
return Root;
|
||||
}
|
||||
}
|
||||
|
||||
bool UMCPVoxelGraphLibrary::IsVoxelGraphAsset(UObject* Asset)
|
||||
{
|
||||
return AsVoxelGraph(Asset) != nullptr;
|
||||
}
|
||||
|
||||
FString UMCPVoxelGraphLibrary::ListTerminalGraphsJson(UObject* Asset)
|
||||
{
|
||||
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
||||
UVoxelGraph* Graph = AsVoxelGraph(Asset);
|
||||
if (!Graph)
|
||||
{
|
||||
Root->SetBoolField(TEXT("ok"), false);
|
||||
Root->SetStringField(TEXT("error"), TEXT("asset is not a UVoxelGraph"));
|
||||
return VoxelJsonStringify(Root);
|
||||
}
|
||||
|
||||
Root->SetBoolField(TEXT("ok"), true);
|
||||
Root->SetStringField(TEXT("asset"), Graph->GetPathName());
|
||||
Root->SetStringField(TEXT("class"), Graph->GetClass()->GetPathName());
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Terminals;
|
||||
Graph->ForeachTerminalGraph_NoInheritance([&](UVoxelTerminalGraph& TerminalGraph)
|
||||
{
|
||||
TSharedRef<FJsonObject> Item = MakeShared<FJsonObject>();
|
||||
FillTerminalJson(Item, TerminalGraph);
|
||||
Terminals.Add(MakeShared<FJsonValueObject>(Item));
|
||||
});
|
||||
Root->SetArrayField(TEXT("terminals"), Terminals);
|
||||
Root->SetNumberField(TEXT("terminal_count"), Terminals.Num());
|
||||
return VoxelJsonStringify(Root);
|
||||
}
|
||||
|
||||
UEdGraph* UMCPVoxelGraphLibrary::FindTerminalEdGraph(UObject* Asset, const FString& Terminal)
|
||||
{
|
||||
UVoxelTerminalGraph* TerminalGraph = FindTerminalGraph(AsVoxelGraph(Asset), Terminal);
|
||||
return TerminalGraph ? &TerminalGraph->GetEdGraph() : nullptr;
|
||||
}
|
||||
|
||||
FString UMCPVoxelGraphLibrary::DumpTerminalGraphJson(UObject* Asset, const FString& Terminal)
|
||||
{
|
||||
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
||||
UVoxelGraph* Graph = AsVoxelGraph(Asset);
|
||||
UVoxelTerminalGraph* TerminalGraph = FindTerminalGraph(Graph, Terminal);
|
||||
if (!Graph || !TerminalGraph)
|
||||
{
|
||||
Root->SetBoolField(TEXT("ok"), false);
|
||||
Root->SetStringField(TEXT("error"), Graph ? TEXT("terminal graph not found") : TEXT("asset is not a UVoxelGraph"));
|
||||
return VoxelJsonStringify(Root);
|
||||
}
|
||||
|
||||
Root->SetBoolField(TEXT("ok"), true);
|
||||
Root->SetStringField(TEXT("asset"), Graph->GetPathName());
|
||||
|
||||
TSharedRef<FJsonObject> TerminalJson = MakeShared<FJsonObject>();
|
||||
FillTerminalJson(TerminalJson, *TerminalGraph);
|
||||
Root->SetObjectField(TEXT("terminal"), TerminalJson);
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Nodes;
|
||||
UEdGraph& EdGraph = TerminalGraph->GetEdGraph();
|
||||
for (UEdGraphNode* Node : EdGraph.Nodes)
|
||||
{
|
||||
if (Node)
|
||||
{
|
||||
Nodes.Add(MakeShared<FJsonValueObject>(VoxelNodeToJson(Node)));
|
||||
}
|
||||
}
|
||||
Root->SetArrayField(TEXT("nodes"), Nodes);
|
||||
return VoxelJsonStringify(Root);
|
||||
}
|
||||
|
||||
FString UMCPVoxelGraphLibrary::GetSelectedNodesJson(UObject* Asset, const FString& Terminal)
|
||||
{
|
||||
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
||||
UEdGraph* EdGraph = FindTerminalEdGraph(Asset, Terminal);
|
||||
if (!EdGraph)
|
||||
{
|
||||
Root->SetBoolField(TEXT("ok"), false);
|
||||
Root->SetStringField(TEXT("error"), TEXT("terminal graph not found"));
|
||||
return VoxelJsonStringify(Root);
|
||||
}
|
||||
|
||||
const TSharedPtr<FVoxelGraphToolkit> Toolkit = FVoxelGraphToolkit::Get(EdGraph);
|
||||
if (!Toolkit.IsValid())
|
||||
{
|
||||
Root->SetBoolField(TEXT("ok"), false);
|
||||
Root->SetStringField(TEXT("error"), TEXT("voxel graph editor is not open"));
|
||||
return VoxelJsonStringify(Root);
|
||||
}
|
||||
|
||||
Root->SetBoolField(TEXT("ok"), true);
|
||||
Root->SetStringField(TEXT("graph"), EdGraph->GetName());
|
||||
TArray<TSharedPtr<FJsonValue>> Nodes;
|
||||
for (UEdGraphNode* Node : Toolkit->GetSelectedNodes())
|
||||
{
|
||||
if (Node)
|
||||
{
|
||||
Nodes.Add(MakeShared<FJsonValueObject>(VoxelNodeToJson(Node)));
|
||||
}
|
||||
}
|
||||
Root->SetArrayField(TEXT("selected"), Nodes);
|
||||
Root->SetNumberField(TEXT("selected_count"), Nodes.Num());
|
||||
return VoxelJsonStringify(Root);
|
||||
}
|
||||
|
||||
FString UMCPVoxelGraphLibrary::SpawnCommentNode(UEdGraph* Graph, FVector2D Position, FVector2D Size, const FString& Comment, FLinearColor Color)
|
||||
{
|
||||
if (!Graph)
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
Graph->Modify();
|
||||
UEdGraphNode_Comment* Node = NewObject<UEdGraphNode_Comment>(Graph);
|
||||
Node->CreateNewGuid();
|
||||
Node->NodePosX = static_cast<int32>(Position.X);
|
||||
Node->NodePosY = static_cast<int32>(Position.Y);
|
||||
Node->NodeWidth = FMath::Max(64, static_cast<int32>(Size.X));
|
||||
Node->NodeHeight = FMath::Max(64, static_cast<int32>(Size.Y));
|
||||
Node->NodeComment = Comment;
|
||||
Node->CommentColor = Color;
|
||||
Node->bCommentBubbleVisible = false;
|
||||
Graph->AddNode(Node, true, false);
|
||||
Node->PostPlacedNewNode();
|
||||
NotifyGraphChanged(Graph, false);
|
||||
return Node->NodeGuid.ToString();
|
||||
}
|
||||
|
||||
FString UMCPVoxelGraphLibrary::ListNodeTypesJson(const FString& Filter, const int32 Limit)
|
||||
{
|
||||
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
||||
Root->SetBoolField(TEXT("ok"), true);
|
||||
|
||||
const FString FilterLower = Filter.ToLower();
|
||||
const int32 EffectiveLimit = Limit > 0 ? Limit : MAX_int32;
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Items;
|
||||
int32 Total = 0;
|
||||
for (const TSharedRef<const FVoxelNode>& Node : GetVoxelNodeCatalogue())
|
||||
{
|
||||
const FString Id = GetNodeId(*Node);
|
||||
const FString DisplayName = Node->GetDisplayName();
|
||||
const FString Category = Node->GetCategory();
|
||||
|
||||
if (!FilterLower.IsEmpty())
|
||||
{
|
||||
const bool bMatches =
|
||||
Id.ToLower().Contains(FilterLower) ||
|
||||
DisplayName.ToLower().Contains(FilterLower) ||
|
||||
Category.ToLower().Contains(FilterLower);
|
||||
if (!bMatches)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
++Total;
|
||||
if (Items.Num() >= EffectiveLimit)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Item = MakeShared<FJsonObject>();
|
||||
Item->SetStringField(TEXT("id"), Id);
|
||||
Item->SetStringField(TEXT("display_name"), DisplayName);
|
||||
Item->SetStringField(TEXT("category"), Category);
|
||||
Item->SetBoolField(TEXT("is_pure"), Node->IsPureNode());
|
||||
Item->SetStringField(TEXT("kind"), GetNodeFunction(*Node) ? TEXT("function") : TEXT("struct"));
|
||||
Items.Add(MakeShared<FJsonValueObject>(Item));
|
||||
}
|
||||
|
||||
Root->SetArrayField(TEXT("nodes"), Items);
|
||||
Root->SetNumberField(TEXT("returned"), Items.Num());
|
||||
Root->SetNumberField(TEXT("total"), Total);
|
||||
Root->SetBoolField(TEXT("truncated"), Total > Items.Num());
|
||||
return VoxelJsonStringify(Root);
|
||||
}
|
||||
|
||||
FString UMCPVoxelGraphLibrary::SpawnStructNode(UEdGraph* Graph, const FString& NodeId, FVector2D Position)
|
||||
{
|
||||
if (!Graph)
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
const TSharedPtr<const FVoxelNode> Chosen = FindCatalogueNode(NodeId);
|
||||
if (!Chosen)
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
UClass* NodeClass = FindObject<UClass>(nullptr, TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_Struct"));
|
||||
if (!NodeClass)
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
Graph->Modify();
|
||||
|
||||
// NewObject with the reflected class avoids referencing the (non-exported) StaticClass()
|
||||
// symbol of UVoxelGraphNode_Struct; the static_cast is purely compile-time.
|
||||
UVoxelGraphNode_Struct* Node = static_cast<UVoxelGraphNode_Struct*>(
|
||||
NewObject<UEdGraphNode>(Graph, NodeClass, NAME_None, RF_Transactional));
|
||||
if (!Node)
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
// Mirrors FVoxelGraphSchemaAction_NewStructNode::PerformAction: copy the catalogue
|
||||
// node into the graph node's instanced Struct, then let it build its pins.
|
||||
Node->Struct = FVoxelInstancedStruct::Make(*Chosen);
|
||||
Node->NodePosX = static_cast<int32>(Position.X);
|
||||
Node->NodePosY = static_cast<int32>(Position.Y);
|
||||
Node->CreateNewGuid();
|
||||
|
||||
Graph->AddNode(Node, true, false);
|
||||
|
||||
Node->PostPlacedNewNode();
|
||||
if (Node->Pins.Num() == 0)
|
||||
{
|
||||
Node->AllocateDefaultPins();
|
||||
}
|
||||
|
||||
return Node->NodeGuid.ToString();
|
||||
}
|
||||
|
||||
FString UMCPVoxelGraphLibrary::AddGraphPropertyJson(UObject* Asset, const FString& Terminal, const FString& Kind, const FString& Name, const FString& TypeStr, const FString& Category)
|
||||
{
|
||||
UVoxelGraph* Graph = AsVoxelGraph(Asset);
|
||||
if (!Graph)
|
||||
{
|
||||
return VoxelJsonStringify(JsonError(TEXT("asset is not a UVoxelGraph")));
|
||||
}
|
||||
|
||||
const FString KindLower = Kind.ToLower();
|
||||
const FVoxelPinType Type = PinTypeFromString(TypeStr);
|
||||
const FName PropName = Name.IsEmpty() ? FName(TEXT("NewProperty")) : FName(*Name);
|
||||
const FGuid Guid = FGuid::NewGuid();
|
||||
|
||||
if (KindLower == TEXT("parameter"))
|
||||
{
|
||||
FVoxelParameter Parameter;
|
||||
Parameter.Name = PropName;
|
||||
Parameter.Type = Type;
|
||||
#if WITH_EDITORONLY_DATA
|
||||
Parameter.Category = Category;
|
||||
#endif
|
||||
Parameter.Fixup();
|
||||
Graph->AddParameter(Guid, Parameter);
|
||||
Graph->Fixup();
|
||||
}
|
||||
else
|
||||
{
|
||||
UVoxelTerminalGraph* TerminalGraph = FindTerminalGraph(Graph, Terminal);
|
||||
if (!TerminalGraph)
|
||||
{
|
||||
return VoxelJsonStringify(JsonError(TEXT("terminal graph not found")));
|
||||
}
|
||||
|
||||
if (KindLower == TEXT("input"))
|
||||
{
|
||||
FVoxelGraphFunctionInput Input;
|
||||
Input.Name = PropName;
|
||||
Input.Type = Type;
|
||||
#if WITH_EDITORONLY_DATA
|
||||
Input.Category = Category;
|
||||
#endif
|
||||
Input.Fixup();
|
||||
TerminalGraph->AddFunctionInput(Guid, Input);
|
||||
}
|
||||
else if (KindLower == TEXT("output"))
|
||||
{
|
||||
FVoxelGraphFunctionOutput Output;
|
||||
Output.Name = PropName;
|
||||
Output.Type = Type;
|
||||
#if WITH_EDITORONLY_DATA
|
||||
Output.Category = Category;
|
||||
#endif
|
||||
TerminalGraph->AddFunctionOutput(Guid, Output);
|
||||
}
|
||||
else if (KindLower == TEXT("local"))
|
||||
{
|
||||
FVoxelGraphLocalVariable LocalVariable;
|
||||
LocalVariable.Name = PropName;
|
||||
LocalVariable.Type = Type;
|
||||
#if WITH_EDITORONLY_DATA
|
||||
LocalVariable.Category = Category;
|
||||
#endif
|
||||
TerminalGraph->AddLocalVariable(Guid, LocalVariable);
|
||||
}
|
||||
else
|
||||
{
|
||||
return VoxelJsonStringify(JsonError(FString::Printf(TEXT("unknown kind %s (use parameter|input|output|local)"), *Kind)));
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
||||
Root->SetBoolField(TEXT("ok"), true);
|
||||
Root->SetStringField(TEXT("guid"), Guid.ToString());
|
||||
Root->SetStringField(TEXT("name"), PropName.ToString());
|
||||
Root->SetStringField(TEXT("type"), Type.ToString());
|
||||
Root->SetStringField(TEXT("kind"), KindLower);
|
||||
return VoxelJsonStringify(Root);
|
||||
}
|
||||
|
||||
FString UMCPVoxelGraphLibrary::ListGraphPropertiesJson(UObject* Asset, const FString& Terminal, const FString& Kind)
|
||||
{
|
||||
UVoxelGraph* Graph = AsVoxelGraph(Asset);
|
||||
if (!Graph)
|
||||
{
|
||||
return VoxelJsonStringify(JsonError(TEXT("asset is not a UVoxelGraph")));
|
||||
}
|
||||
|
||||
const FString KindLower = Kind.ToLower();
|
||||
TArray<TSharedPtr<FJsonValue>> Items;
|
||||
|
||||
const auto AddItem = [&Items](const FGuid& Guid, const FName& PropName, const FVoxelPinType& Type, const FString& Category)
|
||||
{
|
||||
TSharedRef<FJsonObject> Item = MakeShared<FJsonObject>();
|
||||
Item->SetStringField(TEXT("guid"), Guid.ToString());
|
||||
Item->SetStringField(TEXT("name"), PropName.ToString());
|
||||
Item->SetStringField(TEXT("type"), Type.ToString());
|
||||
Item->SetStringField(TEXT("category"), Category);
|
||||
Items.Add(MakeShared<FJsonValueObject>(Item));
|
||||
};
|
||||
|
||||
if (KindLower == TEXT("parameter"))
|
||||
{
|
||||
for (const FGuid& G : Graph->GetParameters())
|
||||
{
|
||||
const FVoxelParameter& P = Graph->FindParameterChecked(G);
|
||||
#if WITH_EDITORONLY_DATA
|
||||
AddItem(G, P.Name, P.Type, P.Category);
|
||||
#else
|
||||
AddItem(G, P.Name, P.Type, FString());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UVoxelTerminalGraph* TerminalGraph = FindTerminalGraph(Graph, Terminal);
|
||||
if (!TerminalGraph)
|
||||
{
|
||||
return VoxelJsonStringify(JsonError(TEXT("terminal graph not found")));
|
||||
}
|
||||
|
||||
if (KindLower == TEXT("input"))
|
||||
{
|
||||
for (const FGuid& G : TerminalGraph->GetFunctionInputs())
|
||||
{
|
||||
const FVoxelGraphFunctionInput& P = TerminalGraph->FindInputChecked(G);
|
||||
#if WITH_EDITORONLY_DATA
|
||||
AddItem(G, P.Name, P.Type, P.Category);
|
||||
#else
|
||||
AddItem(G, P.Name, P.Type, FString());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else if (KindLower == TEXT("output"))
|
||||
{
|
||||
for (const FGuid& G : TerminalGraph->GetFunctionOutputs())
|
||||
{
|
||||
const FVoxelGraphFunctionOutput& P = TerminalGraph->FindOutputChecked(G);
|
||||
#if WITH_EDITORONLY_DATA
|
||||
AddItem(G, P.Name, P.Type, P.Category);
|
||||
#else
|
||||
AddItem(G, P.Name, P.Type, FString());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else if (KindLower == TEXT("local"))
|
||||
{
|
||||
for (const FGuid& G : TerminalGraph->GetLocalVariables())
|
||||
{
|
||||
const FVoxelGraphLocalVariable& P = TerminalGraph->FindLocalVariableChecked(G);
|
||||
#if WITH_EDITORONLY_DATA
|
||||
AddItem(G, P.Name, P.Type, P.Category);
|
||||
#else
|
||||
AddItem(G, P.Name, P.Type, FString());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return VoxelJsonStringify(JsonError(FString::Printf(TEXT("unknown kind %s (use parameter|input|output|local)"), *Kind)));
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
||||
Root->SetBoolField(TEXT("ok"), true);
|
||||
Root->SetStringField(TEXT("kind"), KindLower);
|
||||
Root->SetArrayField(TEXT("properties"), Items);
|
||||
Root->SetNumberField(TEXT("count"), Items.Num());
|
||||
return VoxelJsonStringify(Root);
|
||||
}
|
||||
|
||||
FString UMCPVoxelGraphLibrary::SpawnPropertyNode(UEdGraph* Graph, const FString& Kind, const FString& Ref, FVector2D Position, bool bDeclaration)
|
||||
{
|
||||
if (!Graph)
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
const FString KindLower = Kind.ToLower();
|
||||
UVoxelTerminalGraph* Terminal = OwningTerminal(Graph);
|
||||
|
||||
FGuid Guid;
|
||||
const TCHAR* ClassPath = nullptr;
|
||||
|
||||
if (KindLower == TEXT("parameter"))
|
||||
{
|
||||
UVoxelGraph* OwnerGraph = Terminal ? &Terminal->GetGraph() : nullptr;
|
||||
Guid = ResolveParameterGuid(OwnerGraph, Ref);
|
||||
ClassPath = TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_Parameter");
|
||||
}
|
||||
else if (KindLower == TEXT("input"))
|
||||
{
|
||||
Guid = ResolveTerminalPropGuid(Terminal, KindLower, Ref);
|
||||
ClassPath = TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_FunctionInput");
|
||||
}
|
||||
else if (KindLower == TEXT("output"))
|
||||
{
|
||||
Guid = ResolveTerminalPropGuid(Terminal, KindLower, Ref);
|
||||
ClassPath = TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_FunctionOutput");
|
||||
}
|
||||
else if (KindLower == TEXT("local"))
|
||||
{
|
||||
Guid = ResolveTerminalPropGuid(Terminal, KindLower, Ref);
|
||||
ClassPath = bDeclaration
|
||||
? TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_LocalVariableDeclaration")
|
||||
: TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_LocalVariableUsage");
|
||||
}
|
||||
else
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
if (!Guid.IsValid())
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
UEdGraphNode* Node = SpawnGuidNodeReflect(Graph, ClassPath, Guid, Position);
|
||||
return Node ? Node->NodeGuid.ToString() : FString();
|
||||
}
|
||||
|
||||
FString UMCPVoxelGraphLibrary::SpawnCallFunctionNode(UEdGraph* Graph, const FString& FunctionRef, FVector2D Position)
|
||||
{
|
||||
UVoxelTerminalGraph* Terminal = OwningTerminal(Graph);
|
||||
UVoxelGraph* OwnerGraph = Terminal ? &Terminal->GetGraph() : nullptr;
|
||||
if (!OwnerGraph)
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
UVoxelTerminalGraph* Function = FindTerminalGraph(OwnerGraph, FunctionRef);
|
||||
if (!Function || Function->IsMainTerminalGraph() || Function->IsEditorTerminalGraph())
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
UEdGraphNode* Node = SpawnGuidNodeReflect(
|
||||
Graph,
|
||||
TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_CallMemberFunction"),
|
||||
Function->GetGuid(),
|
||||
Position);
|
||||
return Node ? Node->NodeGuid.ToString() : FString();
|
||||
}
|
||||
|
||||
bool UMCPVoxelGraphLibrary::NotifyGraphChanged(UEdGraph* Graph, const bool bCompile)
|
||||
{
|
||||
if (!Graph)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UVoxelTerminalGraph* TerminalGraph = Graph->GetTypedOuter<UVoxelTerminalGraph>();
|
||||
if (!TerminalGraph)
|
||||
{
|
||||
Graph->MarkPackageDirty();
|
||||
return false;
|
||||
}
|
||||
|
||||
TerminalGraph->Modify();
|
||||
TerminalGraph->GetGraph().Modify();
|
||||
Graph->Modify();
|
||||
Graph->MarkPackageDirty();
|
||||
TerminalGraph->GetGraph().MarkPackageDirty();
|
||||
|
||||
if (GVoxelGraphTracker)
|
||||
{
|
||||
GVoxelGraphTracker->NotifyEdGraphChanged(*Graph);
|
||||
GVoxelGraphTracker->NotifyTerminalGraphChanged(TerminalGraph->GetGraph());
|
||||
}
|
||||
|
||||
if (bCompile)
|
||||
{
|
||||
TerminalGraph->GetRuntime().EnsureIsCompiled(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UMCPVoxelGraphLibrary::OpenVoxelGraphAsset(UObject* Asset)
|
||||
{
|
||||
if (!Asset || !GEditor)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
|
||||
return AssetEditorSubsystem && AssetEditorSubsystem->OpenEditorForAsset(Asset);
|
||||
}
|
||||
|
||||
#else // !WITH_MCP_VOXEL
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Voxel plugin not present in this project. Provide stub implementations so the
|
||||
// module always links. voxel_graph_op will report this error to the caller.
|
||||
// Rebuild with the Voxel plugin installed (or env UE_MCP_WITH_VOXEL=1) to enable.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static const TCHAR* MCPVoxelDisabledJson()
|
||||
{
|
||||
return TEXT("{\"ok\":false,\"error\":\"Voxel plugin not enabled in this build. Install the Voxel plugin and rebuild (UE_MCP_WITH_VOXEL=1) to use voxel_graph_op.\"}");
|
||||
}
|
||||
|
||||
bool UMCPVoxelGraphLibrary::IsVoxelGraphAsset(UObject*) { return false; }
|
||||
FString UMCPVoxelGraphLibrary::ListTerminalGraphsJson(UObject*) { return MCPVoxelDisabledJson(); }
|
||||
UEdGraph* UMCPVoxelGraphLibrary::FindTerminalEdGraph(UObject*, const FString&) { return nullptr; }
|
||||
FString UMCPVoxelGraphLibrary::DumpTerminalGraphJson(UObject*, const FString&) { return MCPVoxelDisabledJson(); }
|
||||
FString UMCPVoxelGraphLibrary::GetSelectedNodesJson(UObject*, const FString&) { return MCPVoxelDisabledJson(); }
|
||||
FString UMCPVoxelGraphLibrary::SpawnCommentNode(UEdGraph*, FVector2D, FVector2D, const FString&, FLinearColor) { return FString(); }
|
||||
FString UMCPVoxelGraphLibrary::ListNodeTypesJson(const FString&, int32) { return MCPVoxelDisabledJson(); }
|
||||
FString UMCPVoxelGraphLibrary::SpawnStructNode(UEdGraph*, const FString&, FVector2D) { return FString(); }
|
||||
FString UMCPVoxelGraphLibrary::AddGraphPropertyJson(UObject*, const FString&, const FString&, const FString&, const FString&, const FString&) { return MCPVoxelDisabledJson(); }
|
||||
FString UMCPVoxelGraphLibrary::ListGraphPropertiesJson(UObject*, const FString&, const FString&) { return MCPVoxelDisabledJson(); }
|
||||
FString UMCPVoxelGraphLibrary::SpawnPropertyNode(UEdGraph*, const FString&, const FString&, FVector2D, bool) { return FString(); }
|
||||
FString UMCPVoxelGraphLibrary::SpawnCallFunctionNode(UEdGraph*, const FString&, FVector2D) { return FString(); }
|
||||
bool UMCPVoxelGraphLibrary::NotifyGraphChanged(UEdGraph*, bool) { return false; }
|
||||
bool UMCPVoxelGraphLibrary::OpenVoxelGraphAsset(UObject*) { return false; }
|
||||
|
||||
#endif // WITH_MCP_VOXEL
|
||||
126
Source/UEBlueprintMCPEditor/Private/MCPWidgetRenderLibrary.cpp
Normal file
126
Source/UEBlueprintMCPEditor/Private/MCPWidgetRenderLibrary.cpp
Normal file
@ -0,0 +1,126 @@
|
||||
#include "MCPWidgetRenderLibrary.h"
|
||||
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "Blueprint/WidgetTree.h"
|
||||
#include "Editor.h"
|
||||
#include "Engine/TextureRenderTarget2D.h"
|
||||
#include "Engine/World.h"
|
||||
#include "HAL/PlatformFileManager.h"
|
||||
#include "IImageWrapper.h"
|
||||
#include "IImageWrapperModule.h"
|
||||
#include "ImageUtils.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "RenderingThread.h"
|
||||
#include "Slate/WidgetRenderer.h"
|
||||
|
||||
bool UMCPWidgetRenderLibrary::RenderWidgetToPNG(
|
||||
TSubclassOf<UUserWidget> WidgetClass,
|
||||
const FString& OutputAbsPath,
|
||||
const FString& FileNameNoExt,
|
||||
int32 Width,
|
||||
int32 Height,
|
||||
FString& OutFullPath,
|
||||
FString& OutError)
|
||||
{
|
||||
OutFullPath.Reset();
|
||||
OutError.Reset();
|
||||
|
||||
if (!WidgetClass)
|
||||
{
|
||||
OutError = TEXT("WidgetClass is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
|
||||
if (!World)
|
||||
{
|
||||
OutError = TEXT("No editor world available");
|
||||
return false;
|
||||
}
|
||||
|
||||
const int32 W = FMath::Clamp(Width, 16, 8192);
|
||||
const int32 H = FMath::Clamp(Height, 16, 8192);
|
||||
|
||||
UUserWidget* Widget = CreateWidget<UUserWidget>(World, WidgetClass);
|
||||
if (!Widget)
|
||||
{
|
||||
OutError = TEXT("CreateWidget returned null");
|
||||
return false;
|
||||
}
|
||||
|
||||
// FWidgetRenderer wants a transient render target. Use a non-asset one.
|
||||
UTextureRenderTarget2D* RT = NewObject<UTextureRenderTarget2D>();
|
||||
RT->ClearColor = FLinearColor(0.f, 0.f, 0.f, 0.f);
|
||||
RT->TargetGamma = 1.f;
|
||||
RT->InitCustomFormat(W, H, PF_B8G8R8A8, /*bForceLinearGamma=*/false);
|
||||
RT->UpdateResourceImmediate(true);
|
||||
|
||||
// Use a software widget renderer so we don't need a live Slate window.
|
||||
FWidgetRenderer* Renderer = new FWidgetRenderer(/*bUseGammaCorrection=*/true);
|
||||
if (!Renderer)
|
||||
{
|
||||
OutError = TEXT("FWidgetRenderer alloc failed");
|
||||
return false;
|
||||
}
|
||||
Renderer->SetIsPrepassNeeded(true);
|
||||
|
||||
const FVector2D DrawSize(W, H);
|
||||
Renderer->DrawWidget(RT, Widget->TakeWidget(), DrawSize, 0.016f, /*bDeferRenderTargetUpdate=*/false);
|
||||
|
||||
// Wait for the GPU work to finish so we can read back pixels.
|
||||
FlushRenderingCommands();
|
||||
|
||||
// Read back pixels.
|
||||
FTextureRenderTargetResource* RTResource = RT->GameThread_GetRenderTargetResource();
|
||||
if (!RTResource)
|
||||
{
|
||||
BeginCleanup(Renderer);
|
||||
OutError = TEXT("RT resource null after render");
|
||||
return false;
|
||||
}
|
||||
|
||||
TArray<FColor> Pixels;
|
||||
Pixels.SetNumUninitialized(W * H);
|
||||
FReadSurfaceDataFlags ReadFlags(RCM_UNorm);
|
||||
ReadFlags.SetLinearToGamma(false);
|
||||
if (!RTResource->ReadPixels(Pixels, ReadFlags))
|
||||
{
|
||||
BeginCleanup(Renderer);
|
||||
OutError = TEXT("ReadPixels failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Encode PNG via ImageWrapper.
|
||||
IImageWrapperModule& ImageWrapperModule =
|
||||
FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
|
||||
TSharedPtr<IImageWrapper> PNG = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG);
|
||||
if (!PNG.IsValid() ||
|
||||
!PNG->SetRaw(Pixels.GetData(), Pixels.Num() * sizeof(FColor), W, H, ERGBFormat::BGRA, 8))
|
||||
{
|
||||
BeginCleanup(Renderer);
|
||||
OutError = TEXT("PNG encode failed");
|
||||
return false;
|
||||
}
|
||||
const TArray64<uint8>& Compressed = PNG->GetCompressed(100);
|
||||
|
||||
// Ensure directory.
|
||||
IPlatformFile& PF = FPlatformFileManager::Get().GetPlatformFile();
|
||||
if (!PF.DirectoryExists(*OutputAbsPath))
|
||||
{
|
||||
PF.CreateDirectoryTree(*OutputAbsPath);
|
||||
}
|
||||
|
||||
const FString FullPath = FPaths::Combine(OutputAbsPath, FileNameNoExt + TEXT(".png"));
|
||||
if (!FFileHelper::SaveArrayToFile(Compressed, *FullPath))
|
||||
{
|
||||
BeginCleanup(Renderer);
|
||||
OutError = FString::Printf(TEXT("SaveArrayToFile failed: %s"), *FullPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
BeginCleanup(Renderer);
|
||||
OutFullPath = FullPath;
|
||||
return true;
|
||||
}
|
||||
347
Source/UEBlueprintMCPEditor/Private/UEBlueprintMCPEditor.cpp
Normal file
347
Source/UEBlueprintMCPEditor/Private/UEBlueprintMCPEditor.cpp
Normal file
@ -0,0 +1,347 @@
|
||||
#include "UEBlueprintMCPEditor.h"
|
||||
|
||||
#include "HttpModule.h"
|
||||
#include "Interfaces/IHttpResponse.h"
|
||||
#include "Misc/CoreDelegates.h"
|
||||
#include "Framework/Notifications/NotificationManager.h"
|
||||
#include "Widgets/Notifications/SNotificationList.h"
|
||||
#include "BlueprintEditor.h"
|
||||
#include "BlueprintEditorContext.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphNode.h"
|
||||
#include "EdGraphNode_Comment.h"
|
||||
#include "Framework/Application/IInputProcessor.h"
|
||||
#include "Framework/Application/SlateApplication.h"
|
||||
#include "Framework/Commands/UIAction.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "ScopedTransaction.h"
|
||||
#include "SGraphPanel.h"
|
||||
#include "ToolMenu.h"
|
||||
#include "ToolMenuEntry.h"
|
||||
#include "ToolMenuSection.h"
|
||||
#include "ToolMenus.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "UEBlueprintMCPEditor"
|
||||
|
||||
namespace
|
||||
{
|
||||
const TCHAR* MCPZoneCommentText = TEXT("MCP Zone");
|
||||
const TCHAR* LegacyMCPZoneCommentText = TEXT("MCP Draft Zone");
|
||||
|
||||
bool IsMCPZoneComment(const UEdGraphNode* Node)
|
||||
{
|
||||
const UEdGraphNode_Comment* CommentNode = Cast<UEdGraphNode_Comment>(Node);
|
||||
return CommentNode
|
||||
&& (CommentNode->NodeComment.Equals(MCPZoneCommentText, ESearchCase::IgnoreCase)
|
||||
|| CommentNode->NodeComment.Equals(LegacyMCPZoneCommentText, ESearchCase::IgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
class FMCPZoneInputProcessor : public IInputProcessor
|
||||
{
|
||||
public:
|
||||
explicit FMCPZoneInputProcessor(FUEBlueprintMCPEditorModule& InOwner)
|
||||
: Owner(InOwner)
|
||||
{
|
||||
}
|
||||
|
||||
virtual void Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor) override
|
||||
{
|
||||
}
|
||||
|
||||
virtual bool HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override
|
||||
{
|
||||
if (!Owner.bMCPZoneModeActive || MouseEvent.GetEffectingButton() != EKeys::LeftMouseButton)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TSharedPtr<FBlueprintEditor> Editor = Owner.ActiveMCPZoneEditor.Pin();
|
||||
if (!Editor.IsValid() || !Editor->GetFocusedGraph())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TSharedPtr<SGraphEditor> GraphEditor = Editor->OpenGraphAndBringToFront(Editor->GetFocusedGraph(), false);
|
||||
SGraphPanel* GraphPanel = GraphEditor.IsValid() ? GraphEditor->GetGraphPanel() : nullptr;
|
||||
if (!GraphPanel)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FGeometry& Geometry = GraphPanel->GetTickSpaceGeometry();
|
||||
const FVector2f ScreenPosition = UE::Slate::CastToVector2f(MouseEvent.GetScreenSpacePosition());
|
||||
if (!Geometry.IsUnderLocation(ScreenPosition))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bDragging = true;
|
||||
StartScreenPosition = ScreenPosition;
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual bool HandleMouseMoveEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override
|
||||
{
|
||||
return bDragging && Owner.bMCPZoneModeActive;
|
||||
}
|
||||
|
||||
virtual bool HandleMouseButtonUpEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override
|
||||
{
|
||||
if (!bDragging || MouseEvent.GetEffectingButton() != EKeys::LeftMouseButton)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bDragging = false;
|
||||
const FVector2f EndScreenPosition = UE::Slate::CastToVector2f(MouseEvent.GetScreenSpacePosition());
|
||||
Owner.TryCreateMCPZoneFromScreenDrag(StartScreenPosition, EndScreenPosition);
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual const TCHAR* GetDebugName() const override
|
||||
{
|
||||
return TEXT("MCPZoneInputProcessor");
|
||||
}
|
||||
|
||||
private:
|
||||
FUEBlueprintMCPEditorModule& Owner;
|
||||
bool bDragging = false;
|
||||
FVector2f StartScreenPosition = FVector2f::ZeroVector;
|
||||
};
|
||||
|
||||
IMPLEMENT_MODULE(FUEBlueprintMCPEditorModule, UEBlueprintMCPEditor)
|
||||
|
||||
void FUEBlueprintMCPEditorModule::StartupModule()
|
||||
{
|
||||
UToolMenus::RegisterStartupCallback(
|
||||
FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FUEBlueprintMCPEditorModule::RegisterMenus));
|
||||
|
||||
FCoreDelegates::OnPostEngineInit.AddRaw(this, &FUEBlueprintMCPEditorModule::NotifyBridgeStatus);
|
||||
}
|
||||
|
||||
void FUEBlueprintMCPEditorModule::NotifyBridgeStatus()
|
||||
{
|
||||
const FString Url = TEXT("http://127.0.0.1:30010/remote/info");
|
||||
TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
|
||||
Request->SetVerb(TEXT("GET"));
|
||||
Request->SetURL(Url);
|
||||
Request->SetTimeout(3.0f);
|
||||
Request->OnProcessRequestComplete().BindLambda(
|
||||
[](FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bSucceeded)
|
||||
{
|
||||
const bool bOk = bSucceeded && Resp.IsValid() && Resp->GetResponseCode() == 200;
|
||||
FNotificationInfo Info(bOk
|
||||
? LOCTEXT("MCPBridgeUp", "UE Blueprint MCP bridge ready (Remote Control :30010)")
|
||||
: LOCTEXT("MCPBridgeDown", "UE Blueprint MCP bridge NOT reachable on :30010 — check Remote Control plugin"));
|
||||
Info.ExpireDuration = bOk ? 4.0f : 8.0f;
|
||||
Info.bUseSuccessFailIcons = true;
|
||||
Info.bFireAndForget = true;
|
||||
TSharedPtr<SNotificationItem> Item = FSlateNotificationManager::Get().AddNotification(Info);
|
||||
if (Item.IsValid())
|
||||
{
|
||||
Item->SetCompletionState(bOk ? SNotificationItem::CS_Success : SNotificationItem::CS_Fail);
|
||||
}
|
||||
});
|
||||
Request->ProcessRequest();
|
||||
}
|
||||
|
||||
void FUEBlueprintMCPEditorModule::ShutdownModule()
|
||||
{
|
||||
FCoreDelegates::OnPostEngineInit.RemoveAll(this);
|
||||
|
||||
if (MCPZoneInputProcessor.IsValid() && FSlateApplication::IsInitialized())
|
||||
{
|
||||
FSlateApplication::Get().UnregisterInputPreProcessor(MCPZoneInputProcessor);
|
||||
MCPZoneInputProcessor.Reset();
|
||||
}
|
||||
|
||||
if (UToolMenus::IsToolMenuUIEnabled())
|
||||
{
|
||||
UToolMenus::UnRegisterStartupCallback(this);
|
||||
UToolMenus::UnregisterOwner(this);
|
||||
}
|
||||
}
|
||||
|
||||
void FUEBlueprintMCPEditorModule::RegisterMenus()
|
||||
{
|
||||
FToolMenuOwnerScoped OwnerScoped(this);
|
||||
UToolMenu* Toolbar = UToolMenus::Get()->ExtendMenu("AssetEditor.BlueprintEditor.ToolBar");
|
||||
FToolMenuSection& Section = Toolbar->FindOrAddSection("Settings");
|
||||
|
||||
Section.AddDynamicEntry("MCPDraftZoneCommands", FNewToolMenuSectionDelegate::CreateLambda([this](FToolMenuSection& InSection)
|
||||
{
|
||||
UBlueprintEditorToolMenuContext* Context = InSection.FindContext<UBlueprintEditorToolMenuContext>();
|
||||
if (!Context || !Context->BlueprintEditor.IsValid() || !Context->GetBlueprintObj())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TWeakPtr<FBlueprintEditor> WeakEditor = Context->BlueprintEditor;
|
||||
FUIAction Action(
|
||||
FExecuteAction::CreateLambda([this, WeakEditor]()
|
||||
{
|
||||
ToggleMCPZoneMode(WeakEditor);
|
||||
}),
|
||||
FCanExecuteAction::CreateLambda([WeakEditor]()
|
||||
{
|
||||
return WeakEditor.IsValid() && WeakEditor.Pin()->GetFocusedGraph() != nullptr;
|
||||
}),
|
||||
FIsActionChecked::CreateLambda([this, WeakEditor]()
|
||||
{
|
||||
return IsMCPZoneModeActive(WeakEditor);
|
||||
}));
|
||||
|
||||
FToolMenuEntry Entry = FToolMenuEntry::InitToolBarButton(
|
||||
FName(TEXT("MCPDraftZone")),
|
||||
FToolUIActionChoice(Action),
|
||||
LOCTEXT("MCPDraftZone_Label", "MCP Zone"),
|
||||
LOCTEXT("MCPDraftZone_Tooltip", "Toggle MCP Zone mode. While active, left-mouse drag in the graph to create a generation zone. Toggle off to remove MCP Zone comments while keeping generated nodes."),
|
||||
FSlateIcon(),
|
||||
EUserInterfaceActionType::ToggleButton);
|
||||
InSection.AddEntry(Entry);
|
||||
}));
|
||||
}
|
||||
|
||||
void FUEBlueprintMCPEditorModule::ToggleMCPZoneMode(TWeakPtr<FBlueprintEditor> BlueprintEditor)
|
||||
{
|
||||
TSharedPtr<FBlueprintEditor> Editor = BlueprintEditor.Pin();
|
||||
if (!Editor.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (bMCPZoneModeActive && ActiveMCPZoneEditor.Pin() == Editor)
|
||||
{
|
||||
RemoveMCPZones(Editor->GetBlueprintObj());
|
||||
bMCPZoneModeActive = false;
|
||||
ActiveMCPZoneEditor.Reset();
|
||||
if (MCPZoneInputProcessor.IsValid() && FSlateApplication::IsInitialized())
|
||||
{
|
||||
FSlateApplication::Get().UnregisterInputPreProcessor(MCPZoneInputProcessor);
|
||||
MCPZoneInputProcessor.Reset();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (bMCPZoneModeActive)
|
||||
{
|
||||
if (TSharedPtr<FBlueprintEditor> PreviousEditor = ActiveMCPZoneEditor.Pin())
|
||||
{
|
||||
RemoveMCPZones(PreviousEditor->GetBlueprintObj());
|
||||
}
|
||||
}
|
||||
|
||||
bMCPZoneModeActive = true;
|
||||
ActiveMCPZoneEditor = Editor;
|
||||
if (!MCPZoneInputProcessor.IsValid() && FSlateApplication::IsInitialized())
|
||||
{
|
||||
MCPZoneInputProcessor = MakeShared<FMCPZoneInputProcessor>(*this);
|
||||
FSlateApplication::Get().RegisterInputPreProcessor(MCPZoneInputProcessor);
|
||||
}
|
||||
}
|
||||
|
||||
bool FUEBlueprintMCPEditorModule::IsMCPZoneModeActive(TWeakPtr<FBlueprintEditor> BlueprintEditor) const
|
||||
{
|
||||
return bMCPZoneModeActive && ActiveMCPZoneEditor.Pin() == BlueprintEditor.Pin();
|
||||
}
|
||||
|
||||
bool FUEBlueprintMCPEditorModule::TryCreateMCPZoneFromScreenDrag(const FVector2f& StartScreenPosition, const FVector2f& EndScreenPosition)
|
||||
{
|
||||
TSharedPtr<FBlueprintEditor> Editor = ActiveMCPZoneEditor.Pin();
|
||||
if (!Editor.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UBlueprint* Blueprint = Editor->GetBlueprintObj();
|
||||
UEdGraph* Graph = Editor->GetFocusedGraph();
|
||||
if (!Blueprint || !Graph)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TSharedPtr<SGraphEditor> GraphEditor = Editor->OpenGraphAndBringToFront(Graph, false);
|
||||
SGraphPanel* GraphPanel = GraphEditor.IsValid() ? GraphEditor->GetGraphPanel() : nullptr;
|
||||
if (!GraphPanel)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FGeometry& Geometry = GraphPanel->GetTickSpaceGeometry();
|
||||
const FVector2f LocalStart = UE::Slate::CastToVector2f(Geometry.AbsoluteToLocal(StartScreenPosition));
|
||||
const FVector2f LocalEnd = UE::Slate::CastToVector2f(Geometry.AbsoluteToLocal(EndScreenPosition));
|
||||
const FVector2f GraphStart = UE::Slate::CastToVector2f(GraphPanel->PanelCoordToGraphCoord(LocalStart));
|
||||
const FVector2f GraphEnd = UE::Slate::CastToVector2f(GraphPanel->PanelCoordToGraphCoord(LocalEnd));
|
||||
|
||||
CreateMCPZone(Graph, Blueprint, GraphStart, GraphEnd);
|
||||
return true;
|
||||
}
|
||||
|
||||
void FUEBlueprintMCPEditorModule::CreateMCPZone(UEdGraph* Graph, UBlueprint* Blueprint, const FVector2f& GraphStart, const FVector2f& GraphEnd) const
|
||||
{
|
||||
if (!Blueprint || !Graph)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FScopedTransaction Transaction(LOCTEXT("CreateMCPDraftZone", "Create MCP Draft Zone"));
|
||||
Graph->Modify();
|
||||
Blueprint->Modify();
|
||||
|
||||
constexpr int32 MinWidth = 240;
|
||||
constexpr int32 MinHeight = 160;
|
||||
const int32 MinX = FMath::FloorToInt(FMath::Min(GraphStart.X, GraphEnd.X));
|
||||
const int32 MinY = FMath::FloorToInt(FMath::Min(GraphStart.Y, GraphEnd.Y));
|
||||
const int32 MaxX = FMath::CeilToInt(FMath::Max(GraphStart.X, GraphEnd.X));
|
||||
const int32 MaxY = FMath::CeilToInt(FMath::Max(GraphStart.Y, GraphEnd.Y));
|
||||
|
||||
UEdGraphNode_Comment* ZoneNode = NewObject<UEdGraphNode_Comment>(Graph);
|
||||
ZoneNode->CreateNewGuid();
|
||||
ZoneNode->NodeComment = MCPZoneCommentText;
|
||||
ZoneNode->bCommentBubbleVisible = false;
|
||||
ZoneNode->CommentColor = FLinearColor(0.05f, 0.32f, 0.85f, 0.35f);
|
||||
ZoneNode->NodePosX = MinX;
|
||||
ZoneNode->NodePosY = MinY;
|
||||
ZoneNode->NodeWidth = FMath::Max(MinWidth, MaxX - MinX);
|
||||
ZoneNode->NodeHeight = FMath::Max(MinHeight, MaxY - MinY);
|
||||
|
||||
Graph->AddNode(ZoneNode, true, false);
|
||||
ZoneNode->PostPlacedNewNode();
|
||||
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
|
||||
}
|
||||
|
||||
void FUEBlueprintMCPEditorModule::RemoveMCPZones(UBlueprint* Blueprint) const
|
||||
{
|
||||
if (!Blueprint)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FScopedTransaction Transaction(LOCTEXT("RemoveMCPDraftZones", "Remove MCP Draft Zones"));
|
||||
Blueprint->Modify();
|
||||
|
||||
TArray<UEdGraph*> Graphs;
|
||||
Blueprint->GetAllGraphs(Graphs);
|
||||
for (UEdGraph* Graph : Graphs)
|
||||
{
|
||||
if (!Graph)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Graph->Modify();
|
||||
TArray<UEdGraphNode*> Nodes = Graph->Nodes;
|
||||
for (UEdGraphNode* Node : Nodes)
|
||||
{
|
||||
if (IsMCPZoneComment(Node))
|
||||
{
|
||||
Graph->RemoveNode(Node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
Reference in New Issue
Block a user