Integration fixes: BaseLevelEntry with full map path, ECC_Camera traces, PP disable, thumbnails

- Replace TArray<FString> BaseLevels with TArray<FPS_Editor_BaseLevelEntry> (DisplayName + MapPath)
  so levels in subfolders (e.g. /Game/Maps/Warehouse) are found correctly
- Add BaseLevelPathMap lookup in MainWidget for Play, combo, and scene load
- Add optional MapPath parameter to LoadBaseLevelAsSublevel
- Switch all LineTrace channels from ECC_Visibility to ECC_Camera for better
  compatibility with characters that have Visibility disabled
- Add W key binding as alias for Z (translate mode) for AZERTY convenience
- Disable sublevel PostProcessVolume WeightedBlendables on load to avoid
  stencil conflicts with the plugin's selection outline
- Fix thumbnail generation: NeverStream + TEXTUREGROUP_UI + force alpha to 255

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 10:34:37 +02:00
parent 7a8eb7f827
commit 83840d1626
12 changed files with 146 additions and 31 deletions

View File

@@ -163,6 +163,7 @@ void APS_Editor_Pawn::CreateInputActions()
IA_TranslateMode = NewObject<UInputAction>(this, TEXT("IA_TranslateMode"));
IA_TranslateMode->ValueType = EInputActionValueType::Boolean;
EditorMappingContext->MapKey(IA_TranslateMode, EKeys::Z);
EditorMappingContext->MapKey(IA_TranslateMode, EKeys::W);
IA_RotateMode = NewObject<UInputAction>(this, TEXT("IA_RotateMode"));
IA_RotateMode->ValueType = EInputActionValueType::Boolean;
@@ -609,7 +610,7 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
FCollisionQueryParams SplineParams;
SplineParams.AddIgnoredActor(this);
const FVector TraceEnd = RayOrigin + RayDir * 100000.0f;
if (GetWorld()->LineTraceSingleByChannel(SplineHit, RayOrigin, TraceEnd, ECC_Visibility, SplineParams))
if (GetWorld()->LineTraceSingleByChannel(SplineHit, RayOrigin, TraceEnd, ECC_Camera, SplineParams))
{
if (Spline->IsCenterCube(SplineHit.GetComponent()))
{
@@ -670,7 +671,7 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
LineParams.AddIgnoredActor(this);
const FVector TraceEnd = RayOrigin + RayDir * 100000.0f;
if (GetWorld()->LineTraceSingleByChannel(LineHit, RayOrigin, TraceEnd, ECC_Visibility, LineParams))
if (GetWorld()->LineTraceSingleByChannel(LineHit, RayOrigin, TraceEnd, ECC_Camera, LineParams))
{
for (APS_Editor_SplineActor* Spline : TubeCandidates)
{
@@ -730,7 +731,7 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
const FVector AddEnd = RayOrigin + RayDir * 100000.0f;
FVector ClickPos;
if (GetWorld()->LineTraceSingleByChannel(AddHit, RayOrigin, AddEnd, ECC_Visibility, AddParams))
if (GetWorld()->LineTraceSingleByChannel(AddHit, RayOrigin, AddEnd, ECC_Camera, AddParams))
{
ClickPos = AddHit.ImpactPoint + FVector(0.0f, 0.0f, 5.0f);
}
@@ -1106,7 +1107,7 @@ void APS_Editor_Pawn::HandleSnapToGround(const FInputActionValue& Value)
}
}
if (GetWorld()->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_Visibility, Params))
if (GetWorld()->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_Camera, Params))
{
UE_LOG(LogTemp, Log, TEXT("PS_Editor: SnapToGround - PointLoc=%s, HitZ=%.2f, HitActor=%s, HitComp=%s"),
*PointLoc.ToString(), Hit.ImpactPoint.Z,
@@ -1291,7 +1292,7 @@ void APS_Editor_Pawn::ComputeOrbitFocalPoint()
FCollisionQueryParams Params;
Params.AddIgnoredActor(this);
if (GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params))
if (GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Camera, Params))
{
OrbitFocalPoint = Hit.ImpactPoint;
OrbitDistance = FVector::Dist(Start, OrbitFocalPoint);

View File

@@ -9,6 +9,7 @@
#include "PS_Editor_SplineActor.h"
#include "PS_Editor_PointPlaceable.h"
#include "Engine/LevelStreamingDynamic.h"
#include "Engine/PostProcessVolume.h"
#include "PS_Editor_GameInstanceSubsystem.h"
#include "GameFramework/GameModeBase.h"
#include "GameFramework/GameStateBase.h"
@@ -61,7 +62,17 @@ void APS_Editor_PlayerController::BeginPlay()
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Restoring from subsystem - BaseLevel='%s', Scenario='%s'"),
*Sub->PendingBaseLevel, *Sub->PendingScenarioName);
LoadBaseLevelAsSublevel(Sub->PendingBaseLevel);
// Resolve full map path from MasterCatalog
FString ResolvedMapPath;
for (const FPS_Editor_BaseLevelEntry& Entry : Master->BaseLevels)
{
if (Entry.DisplayName == Sub->PendingBaseLevel)
{
ResolvedMapPath = Entry.MapPath;
break;
}
}
LoadBaseLevelAsSublevel(Sub->PendingBaseLevel, ResolvedMapPath);
if (!Sub->PendingScenarioName.IsEmpty() && SpawnManager && SceneSerializer)
{
@@ -197,7 +208,7 @@ void APS_Editor_PlayerController::CancelPointPlacement()
FinishPointPlacement();
}
void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelName)
void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelName, const FString& MapPath)
{
// Unload previous sublevel first
if (CurrentSublevel)
@@ -238,11 +249,14 @@ void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelNa
return;
}
// Use MapPath if provided, otherwise fall back to LevelName (legacy behavior)
const FString& LevelToLoad = MapPath.IsEmpty() ? LevelName : MapPath;
// Load new sublevel
bool bSuccess = false;
CurrentSublevel = ULevelStreamingDynamic::LoadLevelInstance(
GetWorld(),
LevelName,
LevelToLoad,
FVector::ZeroVector,
FRotator::ZeroRotator,
bSuccess);
@@ -252,11 +266,33 @@ void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelNa
CurrentSublevel->SetShouldBeLoaded(true);
CurrentSublevel->SetShouldBeVisible(true);
CurrentBaseLevel = LevelName;
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Loaded sublevel '%s'"), *LevelName);
// Disable custom PP materials once sublevel actors are available
CurrentSublevel->OnLevelShown.AddUniqueDynamic(this, &APS_Editor_PlayerController::DisableSublevelPostProcessMaterials);
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Loaded sublevel '%s' (path: '%s')"), *LevelName, *LevelToLoad);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Failed to load sublevel '%s'"), *LevelName);
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Failed to load sublevel '%s' (path: '%s')"), *LevelName, *LevelToLoad);
}
}
void APS_Editor_PlayerController::DisableSublevelPostProcessMaterials()
{
if (!CurrentSublevel) return;
ULevel* Level = CurrentSublevel->GetLoadedLevel();
if (!Level) return;
for (AActor* Actor : Level->Actors)
{
if (!Actor) continue;
APostProcessVolume* PPVolume = Cast<APostProcessVolume>(Actor);
if (!PPVolume) continue;
PPVolume->Settings.WeightedBlendables.Array.Empty();
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Disabled PP materials on '%s'"), *PPVolume->GetName());
}
}
@@ -276,7 +312,7 @@ void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bA
const FVector End = WorldOrigin + WorldDirection * 100000.0f;
FVector ClickWorldPos;
if (GetWorld()->LineTraceSingleByChannel(Hit, WorldOrigin, End, ECC_Visibility, Params))
if (GetWorld()->LineTraceSingleByChannel(Hit, WorldOrigin, End, ECC_Camera, Params))
{
ClickWorldPos = Hit.ImpactPoint + FVector(0.0f, 0.0f, 5.0f);
}
@@ -326,7 +362,7 @@ void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bA
TubeParams.AddIgnoredActor(GetPawn());
const FVector TubeEnd = WorldOrigin + WorldDirection * 100000.0f;
if (GetWorld()->LineTraceSingleByChannel(TubeHit, WorldOrigin, TubeEnd, ECC_Visibility, TubeParams))
if (GetWorld()->LineTraceSingleByChannel(TubeHit, WorldOrigin, TubeEnd, ECC_Camera, TubeParams))
{
if (Spline->IsSplineMesh(TubeHit.GetComponent()))
{

View File

@@ -47,9 +47,13 @@ public:
UPROPERTY(BlueprintReadWrite, Category = "PS Editor")
FString CurrentBaseLevel;
/** Load a base level as sublevel for visual context. Unloads previous sublevel. */
/** Load a base level as sublevel for visual context. Unloads previous sublevel.
* @param LevelName Display name stored in CurrentBaseLevel (used for scene filtering).
* @param MapPath Full package path for LoadLevelInstance (e.g. "/Game/Maps/Warehouse").
* If empty, LevelName is used as-is (legacy behavior).
*/
UFUNCTION(BlueprintCallable, Category = "PS Editor")
void LoadBaseLevelAsSublevel(const FString& LevelName);
void LoadBaseLevelAsSublevel(const FString& LevelName, const FString& MapPath = TEXT(""));
// ---- Editor Mode (Normal / SplinePlacement) ----
@@ -119,5 +123,9 @@ private:
UPROPERTY()
TObjectPtr<ULevelStreamingDynamic> CurrentSublevel;
/** Disable custom post-process materials in the loaded sublevel to avoid stencil conflicts. */
UFUNCTION()
void DisableSublevelPostProcessMaterials();
void HandleEditorClick(FVector2D ScreenPos, bool bAdditive);
};

View File

@@ -13,7 +13,7 @@ static void ConfigureGizmoComponent(UPrimitiveComponent* Comp)
{
Comp->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
Comp->SetCollisionResponseToAllChannels(ECR_Ignore);
Comp->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
Comp->SetCollisionResponseToChannel(ECC_Camera, ECR_Block);
Comp->SetCollisionObjectType(ECC_WorldDynamic);
Comp->SetCastShadow(false);
Comp->SetRenderCustomDepth(false);

View File

@@ -127,7 +127,7 @@ void UPS_Editor_SelectionManager::HandleClickAtScreenPosition(FVector2D ScreenPo
}
const FVector End = WorldOrigin + WorldDirection * 100000.0f;
const bool bHit = PC->GetWorld()->LineTraceSingleByChannel(Hit, WorldOrigin, End, ECC_Visibility, Params);
const bool bHit = PC->GetWorld()->LineTraceSingleByChannel(Hit, WorldOrigin, End, ECC_Camera, Params);
if (bHit && Hit.GetActor())
{
@@ -467,7 +467,7 @@ void UPS_Editor_SelectionManager::SnapSelectionToGround()
Params.AddIgnoredActor(PC->GetPawn());
if (Gizmo) Params.AddIgnoredActor(Gizmo);
if (Actor->GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params))
if (Actor->GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Camera, Params))
{
FVector NewLocation = Hit.ImpactPoint;
NewLocation.Z += BottomOffset;

View File

@@ -85,11 +85,18 @@ void UPS_Editor_SpawnCatalog::CaptureAllThumbnails()
uint8* MipData = Texture->Source.LockMip(0);
FMemory::Memcpy(MipData, PixelData.GetData(), PixelData.Num());
// Force alpha to 255 — UE thumbnails may have zeroed alpha
for (int32 Px = 0; Px < Width * Height; ++Px)
{
MipData[Px * 4 + 3] = 255;
}
Texture->Source.UnlockMip(0);
Texture->SRGB = true;
Texture->CompressionSettings = TC_Default;
Texture->MipGenSettings = TMGS_NoMipmaps;
Texture->NeverStream = true;
Texture->LODGroup = TEXTUREGROUP_UI;
Texture->PostEditChange();
Texture->UpdateResource();

View File

@@ -44,6 +44,28 @@ public:
#endif
};
/**
* A base level entry: display name + full map path.
* The MapPath is used for OpenLevel and sublevel loading.
* Example: DisplayName="Warehouse", MapPath="/Game/Maps/Warehouse"
*/
USTRUCT(BlueprintType)
struct FPS_Editor_BaseLevelEntry
{
GENERATED_BODY()
/** Display name shown in the editor combo box and used for scene filtering. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
FString DisplayName;
/**
* Full map path used for OpenLevel and sublevel loading.
* Example: "/Game/Maps/Warehouse"
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
FString MapPath;
};
/**
* Master catalog that groups multiple SpawnCatalogs.
* Assign this to the PlayerController. Each sub-catalog can represent a domain
@@ -60,10 +82,10 @@ public:
TArray<TObjectPtr<UPS_Editor_SpawnCatalog>> Catalogs;
/**
* List of base level names available for scene editing.
* Used to filter saved scenes by level. Auto-matched by stripping "_Editor" from the current map name.
* Example: "Warehouse", "Office", "Parking"
* List of base levels available for scene editing.
* DisplayName is used for UI display and scene filtering.
* MapPath is the full package path for OpenLevel (e.g. "/Game/Maps/Warehouse").
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
TArray<FString> BaseLevels;
TArray<FPS_Editor_BaseLevelEntry> BaseLevels;
};

View File

@@ -67,6 +67,11 @@ void UPS_Editor_SpawnManager::AddCatalog(UPS_Editor_SpawnCatalog* InCatalog)
Resolved.Category = SpawnComp ? SpawnComp->Category : TEXT("Default");
Resolved.Thumbnail = SpawnComp ? SpawnComp->Thumbnail : nullptr;
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolved '%s' - SpawnComp=%s, Thumbnail=%s"),
*Resolved.DisplayName,
SpawnComp ? TEXT("found") : TEXT("null"),
Resolved.Thumbnail ? *Resolved.Thumbnail->GetPathName() : TEXT("null"));
ResolvedEntries.Add(Resolved);
}

View File

@@ -111,11 +111,18 @@ FReply FPS_Editor_SpawnableComponentCustomization::OnCaptureThumbnailClicked()
uint8* MipData = Texture->Source.LockMip(0);
FMemory::Memcpy(MipData, PixelData.GetData(), PixelData.Num());
// Force alpha to 255 — UE thumbnails may have zeroed alpha
for (int32 Px = 0; Px < Width * Height; ++Px)
{
MipData[Px * 4 + 3] = 255;
}
Texture->Source.UnlockMip(0);
Texture->SRGB = true;
Texture->CompressionSettings = TC_Default;
Texture->MipGenSettings = TMGS_NoMipmaps;
Texture->NeverStream = true;
Texture->LODGroup = TEXTUREGROUP_UI;
Texture->PostEditChange();
Texture->UpdateResource();

View File

@@ -27,7 +27,7 @@ APS_Editor_SplineActor::APS_Editor_SplineActor()
SplineMeshComp->SetupAttachment(Root);
SplineMeshComp->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
SplineMeshComp->SetCollisionResponseToAllChannels(ECR_Ignore);
SplineMeshComp->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
SplineMeshComp->SetCollisionResponseToChannel(ECC_Camera, ECR_Block);
SplineMeshComp->bUseComplexAsSimpleCollision = true;
SplineMeshComp->SetCastShadow(false);
@@ -101,7 +101,7 @@ void APS_Editor_SplineActor::BeginPlay()
CenterCube->SetWorldScale3D(FVector(CenterCubeScale));
CenterCube->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
CenterCube->SetCollisionResponseToAllChannels(ECR_Ignore);
CenterCube->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
CenterCube->SetCollisionResponseToChannel(ECC_Camera, ECR_Block);
CenterCube->SetCastShadow(false);
if (CenterCubeMat)
{
@@ -316,7 +316,7 @@ UStaticMeshComponent* APS_Editor_SplineActor::CreatePointHandle(const FVector& W
// Collision for raycast
Handle->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
Handle->SetCollisionResponseToAllChannels(ECR_Ignore);
Handle->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
Handle->SetCollisionResponseToChannel(ECC_Camera, ECR_Block);
Handle->SetCastShadow(false);
Handle->SetRenderInDepthPass(false);

View File

@@ -624,8 +624,16 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
EditorPC->bShowMouseCursor = false;
EditorPC->SetInputMode(FInputModeGameOnly());
// Use full path to avoid confusion with sublevel instances
FString MapPath = FString::Printf(TEXT("/Game/%s"), *BaseLevel);
// Use full path from MasterCatalog, fallback to /Game/{name}
FString MapPath;
if (FString* Found = BaseLevelPathMap.Find(BaseLevel))
{
MapPath = *Found;
}
else
{
MapPath = FString::Printf(TEXT("/Game/%s"), *BaseLevel);
}
UGameplayStatics::OpenLevel(EditorPC, FName(*MapPath), true, TEXT(""));
return FReply::Handled();
})
@@ -676,7 +684,12 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
{
if (EditorPC->CurrentBaseLevel != SceneLevelName)
{
EditorPC->LoadBaseLevelAsSublevel(SceneLevelName);
FString SceneMapPath = SceneLevelName;
if (FString* Found = BaseLevelPathMap.Find(SceneLevelName))
{
SceneMapPath = *Found;
}
EditorPC->LoadBaseLevelAsSublevel(SceneLevelName, SceneMapPath);
CurrentLevelFilter = EditorPC->CurrentBaseLevel;
for (const TSharedPtr<FString>& Opt : BaseLevelOptions)
{
@@ -748,7 +761,12 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
SaveNameField->SetText(FText::GetEmpty());
}
EditorPC->LoadBaseLevelAsSublevel(*Selected);
FString SublevelPath = *Selected;
if (FString* Found = BaseLevelPathMap.Find(*Selected))
{
SublevelPath = *Found;
}
EditorPC->LoadBaseLevelAsSublevel(*Selected, SublevelPath);
CurrentLevelFilter = EditorPC->CurrentBaseLevel;
if (BaseLevelComboText.IsValid())
@@ -834,9 +852,13 @@ void UPS_Editor_MainWidget::NativeConstruct()
BaseLevelOptions.Add(MakeShared<FString>(TEXT("None")));
if (UPS_Editor_MasterCatalog* Master = EditorPC->MasterCatalogAsset)
{
for (const FString& Level : Master->BaseLevels)
for (const FPS_Editor_BaseLevelEntry& Entry : Master->BaseLevels)
{
BaseLevelOptions.Add(MakeShared<FString>(Level));
BaseLevelOptions.Add(MakeShared<FString>(Entry.DisplayName));
if (!Entry.MapPath.IsEmpty())
{
BaseLevelPathMap.Add(Entry.DisplayName, Entry.MapPath);
}
}
}
@@ -915,7 +937,12 @@ void UPS_Editor_MainWidget::PopulateSceneList()
{
if (EditorPC->CurrentBaseLevel != SceneLevelName)
{
EditorPC->LoadBaseLevelAsSublevel(SceneLevelName);
FString SceneMapPath = SceneLevelName;
if (FString* Found = BaseLevelPathMap.Find(SceneLevelName))
{
SceneMapPath = *Found;
}
EditorPC->LoadBaseLevelAsSublevel(SceneLevelName, SceneMapPath);
CurrentLevelFilter = EditorPC->CurrentBaseLevel;
for (const TSharedPtr<FString>& Opt : BaseLevelOptions)

View File

@@ -75,6 +75,8 @@ private:
TArray<TSharedPtr<FString>> BaseLevelOptions;
TSharedPtr<STextBlock> BaseLevelComboText;
FString CurrentLevelFilter;
/** Maps DisplayName → full MapPath for base levels (from MasterCatalog). */
TMap<FString, FString> BaseLevelPathMap;
TArray<TSharedPtr<FSlateBrush>> CachedBrushes;
// Transform fields