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>
127 lines
3.5 KiB
C++
127 lines
3.5 KiB
C++
#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;
|
|
}
|