Ajout du projet Depths sur Git
This commit is contained in:
+319
@@ -0,0 +1,319 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "ProceduralDungeonEdMode.h"
|
||||
#include "ProceduralDungeonEditor.h"
|
||||
#include "Toolkits/ToolkitManager.h"
|
||||
#include "EditorModeManager.h"
|
||||
#include "Engine/LevelScriptBlueprint.h"
|
||||
#include "ProceduralDungeonEdModeToolkit.h"
|
||||
#include "ProceduralDungeonEdLog.h"
|
||||
#include "ProceduralDungeonEditorSettings.h"
|
||||
#include "ProceduralDungeonEditorObject.h"
|
||||
#include "Tools/ProceduralDungeonEditorTool_Size.h"
|
||||
#include "Tools/ProceduralDungeonEditorTool_Door.h"
|
||||
#include "Room.h"
|
||||
#include "RoomLevel.h"
|
||||
#include "RoomData.h"
|
||||
|
||||
#define ROUTE_TO_TOOL(FuncCall) ActiveTool ? ActiveTool->FuncCall : FEdMode::FuncCall
|
||||
|
||||
const FEditorModeID FProceduralDungeonEdMode::EM_ProceduralDungeon(TEXT("EM_ProceduralDungeon"));
|
||||
|
||||
FProceduralDungeonEdMode::FProceduralDungeonEdMode()
|
||||
: FEdMode()
|
||||
{
|
||||
Tools.Add(MakeUnique<FProceduralDungeonEditorTool_Size>(this));
|
||||
Tools.Add(MakeUnique<FProceduralDungeonEditorTool_Door>(this));
|
||||
|
||||
Settings = NewObject<UProceduralDungeonEditorObject>(GetTransientPackage(), TEXT("Editor Settings"), RF_Transactional);
|
||||
}
|
||||
|
||||
void FProceduralDungeonEdMode::AddReferencedObjects(FReferenceCollector& Collector)
|
||||
{
|
||||
FEdMode::AddReferencedObjects(Collector);
|
||||
Collector.AddReferencedObject(Settings);
|
||||
}
|
||||
|
||||
void FProceduralDungeonEdMode::Enter()
|
||||
{
|
||||
DungeonEd_LogInfo("Enter Room Editor Mode.");
|
||||
FEdMode::Enter();
|
||||
|
||||
if (!Toolkit.IsValid())
|
||||
{
|
||||
Toolkit = MakeShareable(new FProceduralDungeonEdModeToolkit);
|
||||
Toolkit->Init(Owner->GetToolkitHost());
|
||||
}
|
||||
|
||||
UpdateLevelBlueprint();
|
||||
|
||||
// Turn on the flag to force the debug drawings.
|
||||
ARoomLevel::bIsDungeonEditorMode = true;
|
||||
}
|
||||
|
||||
void FProceduralDungeonEdMode::Exit()
|
||||
{
|
||||
RegisterLevelCompilationDelegate(false);
|
||||
|
||||
FToolkitManager::Get().CloseToolkit(Toolkit.ToSharedRef());
|
||||
Toolkit.Reset();
|
||||
|
||||
if (ActiveTool)
|
||||
{
|
||||
ActiveTool->ExitTool();
|
||||
ActiveTool = nullptr;
|
||||
}
|
||||
|
||||
CachedLevelInstance.Reset();
|
||||
CachedLevelBlueprint.Reset();
|
||||
|
||||
ARoomLevel::bIsDungeonEditorMode = false;
|
||||
|
||||
FEdMode::Exit();
|
||||
DungeonEd_LogInfo("Exit Room Editor Mode.");
|
||||
}
|
||||
|
||||
void FProceduralDungeonEdMode::Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI)
|
||||
{
|
||||
if (ActiveTool)
|
||||
ActiveTool->Render(View, Viewport, PDI);
|
||||
|
||||
FEdMode::Render(View, Viewport, PDI);
|
||||
}
|
||||
|
||||
void FProceduralDungeonEdMode::Tick(FEditorViewportClient* ViewportClient, float DeltaTime)
|
||||
{
|
||||
FEdMode::Tick(ViewportClient, DeltaTime);
|
||||
|
||||
if (ActiveTool)
|
||||
ActiveTool->Tick(ViewportClient, DeltaTime);
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEdMode::HandleClick(FEditorViewportClient* InViewportClient, HHitProxy* HitProxy, const FViewportClick& Click)
|
||||
{
|
||||
return ROUTE_TO_TOOL(HandleClick(InViewportClient, HitProxy, Click));
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEdMode::InputKey(FEditorViewportClient* ViewportClient, FViewport* Viewport, FKey Key, EInputEvent Event)
|
||||
{
|
||||
return ROUTE_TO_TOOL(InputKey(ViewportClient, Viewport, Key, Event));
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEdMode::InputAxis(FEditorViewportClient* InViewportClient, FViewport* Viewport, int32 ControllerId, FKey Key, float Delta, float DeltaTime)
|
||||
{
|
||||
return ROUTE_TO_TOOL(InputAxis(InViewportClient, Viewport, ControllerId, Key, Delta, DeltaTime));
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEdMode::InputDelta(FEditorViewportClient* InViewportClient, FViewport* InViewport, FVector& InDrag, FRotator& InRot, FVector& InScale)
|
||||
{
|
||||
return ROUTE_TO_TOOL(InputDelta(InViewportClient, InViewport, InDrag, InRot, InScale));
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEdMode::MouseMove(FEditorViewportClient* ViewportClient, FViewport* Viewport, int32 MouseX, int32 MouseY)
|
||||
{
|
||||
return ROUTE_TO_TOOL(MouseMove(ViewportClient, Viewport, MouseX, MouseY));
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEdMode::ShowModeWidgets() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEdMode::ShouldDrawWidget() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEdMode::UsesTransformWidget() const
|
||||
{
|
||||
return ROUTE_TO_TOOL(UsesTransformWidget());
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEdMode::UsesTransformWidget(WidgetMode CheckMode) const
|
||||
{
|
||||
return ROUTE_TO_TOOL(UsesTransformWidget(CheckMode));
|
||||
}
|
||||
|
||||
FVector FProceduralDungeonEdMode::GetWidgetLocation() const
|
||||
{
|
||||
return ROUTE_TO_TOOL(GetWidgetLocation());
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEdMode::GetPivotForOrbit(FVector& OutPivot) const
|
||||
{
|
||||
const auto* PluginSettings = GetDefault<UProceduralDungeonEditorSettings>();
|
||||
if (!PluginSettings->bUseRoomAsOrbitPivot)
|
||||
return false;
|
||||
|
||||
if (!CachedLevelInstance.IsValid())
|
||||
return false;
|
||||
|
||||
URoomData* Data = CachedLevelInstance->Data;
|
||||
if (!IsValid(Data))
|
||||
return false;
|
||||
|
||||
FBoxCenterAndExtent RoomBounds = Data->GetBounds();
|
||||
OutPivot = RoomBounds.Center;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEdMode::GetCursor(EMouseCursor::Type& OutCursor) const
|
||||
{
|
||||
return ROUTE_TO_TOOL(GetCursor(OutCursor));
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEdMode::GetTool(FName ToolName, FProceduralDungeonEditorTool*& OutTool) const
|
||||
{
|
||||
for (auto& Tool : Tools)
|
||||
{
|
||||
if (Tool.IsValid() && Tool->GetToolName() == ToolName)
|
||||
{
|
||||
OutTool = Tool.Get();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FProceduralDungeonEditorTool* FProceduralDungeonEdMode::GetActiveTool() const
|
||||
{
|
||||
return ActiveTool;
|
||||
}
|
||||
|
||||
void FProceduralDungeonEdMode::SetActiveTool(FName ToolName)
|
||||
{
|
||||
if (ActiveTool && ActiveTool->GetToolName() == ToolName)
|
||||
return;
|
||||
|
||||
FProceduralDungeonEditorTool* NewTool = nullptr;
|
||||
if (!GetTool(ToolName, NewTool))
|
||||
{
|
||||
DungeonEd_LogError("Tool '%s' is not a valid tool.", *ToolName.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
check(NewTool);
|
||||
SetActiveTool(NewTool);
|
||||
}
|
||||
|
||||
void FProceduralDungeonEdMode::ResetActiveTool()
|
||||
{
|
||||
SetActiveTool(nullptr);
|
||||
}
|
||||
|
||||
void FProceduralDungeonEdMode::SetActiveTool(FProceduralDungeonEditorTool* NewTool)
|
||||
{
|
||||
if (ActiveTool)
|
||||
ActiveTool->ExitTool();
|
||||
|
||||
DungeonEd_LogInfo("Set active tool to '%s'.", NewTool ? NewTool->GetToolName() : TEXT("None"));
|
||||
ActiveTool = NewTool;
|
||||
|
||||
if (ActiveTool)
|
||||
ActiveTool->EnterTool();
|
||||
}
|
||||
|
||||
void FProceduralDungeonEdMode::SetDefaultTool()
|
||||
{
|
||||
if (!ActiveTool && IsToolEnabled("Tool_Size"))
|
||||
SetActiveTool("Tool_Size");
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEdMode::IsToolEnabled(FName ToolName) const
|
||||
{
|
||||
auto Level = GetLevel();
|
||||
return Level.IsValid() && IsValid(Level->Data);
|
||||
}
|
||||
|
||||
ULevelScriptBlueprint* FProceduralDungeonEdMode::GetLevelBlueprint(bool bCreate) const
|
||||
{
|
||||
UWorld* World = GetWorld();
|
||||
check(World);
|
||||
ULevelScriptBlueprint* LevelBlueprint = World->PersistentLevel->GetLevelScriptBlueprint(/*bDontCreate = */ !bCreate);
|
||||
return LevelBlueprint;
|
||||
}
|
||||
|
||||
TWeakObjectPtr<ARoomLevel> FProceduralDungeonEdMode::GetLevel() const
|
||||
{
|
||||
ULevelScriptBlueprint* LevelBlueprint = GetLevelBlueprint();
|
||||
if (!IsValid(LevelBlueprint))
|
||||
return nullptr;
|
||||
return Cast<ARoomLevel>(LevelBlueprint->GeneratedClass->GetDefaultObject());
|
||||
}
|
||||
|
||||
void FProceduralDungeonEdMode::UpdateLevelBlueprint()
|
||||
{
|
||||
ULevelScriptBlueprint* LevelBlueprint = GetLevelBlueprint();
|
||||
if (CachedLevelBlueprint == LevelBlueprint)
|
||||
return;
|
||||
|
||||
if (CachedLevelBlueprint.IsValid())
|
||||
RegisterLevelCompilationDelegate(false);
|
||||
|
||||
CachedLevelBlueprint = LevelBlueprint;
|
||||
|
||||
if (CachedLevelBlueprint.IsValid())
|
||||
RegisterLevelCompilationDelegate(true);
|
||||
|
||||
OnLevelBlueprintCompiled();
|
||||
}
|
||||
|
||||
void FProceduralDungeonEdMode::OnLevelBlueprintCompiled(UBlueprint* Blueprint)
|
||||
{
|
||||
CachedLevelInstance = Cast<ARoomLevel>(GetWorld()->GetLevelScriptActor());
|
||||
|
||||
auto Level = GetLevel();
|
||||
DungeonEd_LogInfo("Room Level: %s", *GetNameSafe(Level.Get()));
|
||||
|
||||
if (Level.IsValid())
|
||||
SetDefaultTool();
|
||||
else
|
||||
ResetActiveTool();
|
||||
|
||||
auto RoomToolkit = (FProceduralDungeonEdModeToolkit*)Toolkit.Get();
|
||||
check(RoomToolkit);
|
||||
RoomToolkit->OnLevelChanged();
|
||||
|
||||
if (ActiveTool)
|
||||
ActiveTool->OnLevelChanged(Level.Get());
|
||||
}
|
||||
|
||||
void FProceduralDungeonEdMode::RegisterLevelCompilationDelegate(bool Register)
|
||||
{
|
||||
if (!CachedLevelBlueprint.IsValid())
|
||||
{
|
||||
DungeonEd_LogWarning("Can't (un)register level blueprint compilation delegate: the level blueprint is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Register)
|
||||
{
|
||||
if (LevelBlueprintDelegateHandle.IsValid())
|
||||
{
|
||||
DungeonEd_LogWarning("Can't register level blueprint compilation delegate: the delegate is already registered.");
|
||||
}
|
||||
else
|
||||
{
|
||||
LevelBlueprintDelegateHandle = CachedLevelBlueprint->OnCompiled().AddRaw(this, &FProceduralDungeonEdMode::OnLevelBlueprintCompiled);
|
||||
DungeonEd_LogInfo("Regitered level blueprint compilation delegate.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (LevelBlueprintDelegateHandle.IsValid())
|
||||
{
|
||||
CachedLevelBlueprint->OnCompiled().Remove(LevelBlueprintDelegateHandle);
|
||||
LevelBlueprintDelegateHandle.Reset();
|
||||
DungeonEd_LogInfo("Unregitered level blueprint compilation delegate.");
|
||||
}
|
||||
else
|
||||
{
|
||||
DungeonEd_LogWarning("Can't unregister level blueprint compilation delegate: the delegate is not registered.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "EdMode.h"
|
||||
#include "ProceduralDungeonEdTypes.h"
|
||||
#include "EditorMode/Tools/ProceduralDungeonEditorTool.h"
|
||||
|
||||
class ARoomLevel;
|
||||
class ULevelScriptBlueprint;
|
||||
|
||||
class FProceduralDungeonEdMode : public FEdMode
|
||||
{
|
||||
public:
|
||||
const static FEditorModeID EM_ProceduralDungeon;
|
||||
|
||||
FProceduralDungeonEdMode();
|
||||
|
||||
/** FGCObject interface */
|
||||
virtual void AddReferencedObjects(FReferenceCollector& Collector) override;
|
||||
|
||||
//~ Begin FEdMode Interface
|
||||
virtual void Enter() override;
|
||||
virtual void Exit() override;
|
||||
virtual void Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI) override;
|
||||
virtual void Tick(FEditorViewportClient* ViewportClient, float DeltaTime) override;
|
||||
virtual bool HandleClick(FEditorViewportClient* InViewportClient, HHitProxy* HitProxy, const FViewportClick& Click) override;
|
||||
virtual bool InputKey(FEditorViewportClient* ViewportClient, FViewport* Viewport, FKey Key, EInputEvent Event) override;
|
||||
virtual bool InputAxis(FEditorViewportClient* InViewportClient, FViewport* Viewport, int32 ControllerId, FKey Key, float Delta, float DeltaTime) override;
|
||||
virtual bool InputDelta(FEditorViewportClient* InViewportClient, FViewport* InViewport, FVector& InDrag, FRotator& InRot, FVector& InScale) override;
|
||||
virtual bool MouseMove(FEditorViewportClient* ViewportClient, FViewport* Viewport, int32 x, int32 y) override;
|
||||
virtual bool ShowModeWidgets() const override;
|
||||
virtual bool ShouldDrawWidget() const override;
|
||||
virtual bool UsesTransformWidget() const override;
|
||||
virtual bool UsesTransformWidget(WidgetMode CheckMode) const;
|
||||
virtual FVector GetWidgetLocation() const override;
|
||||
virtual bool AllowWidgetMove() override { return true; }
|
||||
virtual bool GetPivotForOrbit(FVector& OutPivot) const override;
|
||||
virtual bool GetCursor(EMouseCursor::Type& OutCursor) const override;
|
||||
//~ End FEdMode Interface
|
||||
|
||||
bool GetTool(FName ToolName, FProceduralDungeonEditorTool*& OutTool) const;
|
||||
FProceduralDungeonEditorTool* GetActiveTool() const;
|
||||
void SetActiveTool(FName ToolName);
|
||||
void ResetActiveTool();
|
||||
void SetDefaultTool();
|
||||
bool IsToolEnabled(FName ToolName) const;
|
||||
|
||||
ULevelScriptBlueprint* GetLevelBlueprint(bool bCreate = false) const;
|
||||
TWeakObjectPtr<ARoomLevel> GetLevel() const;
|
||||
TWeakObjectPtr<ARoomLevel> GetLevelInstance() const { return CachedLevelInstance; }
|
||||
void UpdateLevelBlueprint();
|
||||
|
||||
protected:
|
||||
void SetActiveTool(FProceduralDungeonEditorTool* NewTool);
|
||||
void OnLevelBlueprintCompiled(UBlueprint* Blueprint = nullptr);
|
||||
void RegisterLevelCompilationDelegate(bool Register);
|
||||
|
||||
public:
|
||||
GC_PTR(class UProceduralDungeonEditorObject) Settings { nullptr };
|
||||
|
||||
private:
|
||||
TArray<TUniquePtr<FProceduralDungeonEditorTool>> Tools;
|
||||
FProceduralDungeonEditorTool* ActiveTool = nullptr;
|
||||
TWeakObjectPtr<ARoomLevel> CachedLevelInstance = nullptr;
|
||||
TWeakObjectPtr<ULevelScriptBlueprint> CachedLevelBlueprint = nullptr;
|
||||
FDelegateHandle LevelBlueprintDelegateHandle;
|
||||
};
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "ProceduralDungeonEdModeToolkit.h"
|
||||
#include "ProceduralDungeonEdMode.h"
|
||||
#include "SProceduralDungeonEdModeWidget.h"
|
||||
#include "EditorModeManager.h"
|
||||
#include "ISinglePropertyView.h"
|
||||
#include "ProceduralDungeonEditorCommands.h"
|
||||
#include "ProceduralDungeonEditorObject.h"
|
||||
#include "ProceduralDungeonEdLog.h"
|
||||
#include "Tools/ProceduralDungeonEditorTool.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "ProceduralDungeonEditor"
|
||||
|
||||
void FProceduralDungeonEdModeToolkit::Init(const TSharedPtr<class IToolkitHost>& InitToolkitHost)
|
||||
{
|
||||
auto NameToCommandMap = FProceduralDungeonEditorCommands::Get().NameToCommandMap;
|
||||
|
||||
TSharedRef<FUICommandList> CommandList = GetToolkitCommands();
|
||||
CommandList->MapAction(NameToCommandMap.FindChecked("Tool_Size"), FUIAction(
|
||||
FExecuteAction::CreateSP(this, &FProceduralDungeonEdModeToolkit::OnChangeTool, FName("Tool_Size")),
|
||||
FCanExecuteAction::CreateSP(this, &FProceduralDungeonEdModeToolkit::IsToolEnabled, FName("Tool_Size")),
|
||||
FIsActionChecked::CreateSP(this, &FProceduralDungeonEdModeToolkit::IsToolActive, FName("Tool_Size")))
|
||||
);
|
||||
|
||||
CommandList->MapAction(NameToCommandMap.FindChecked("Tool_Door"), FUIAction(
|
||||
FExecuteAction::CreateSP(this, &FProceduralDungeonEdModeToolkit::OnChangeTool, FName("Tool_Door")),
|
||||
FCanExecuteAction::CreateSP(this, &FProceduralDungeonEdModeToolkit::IsToolEnabled, FName("Tool_Door")),
|
||||
FIsActionChecked::CreateSP(this, &FProceduralDungeonEdModeToolkit::IsToolActive, FName("Tool_Door")))
|
||||
);
|
||||
|
||||
SAssignNew(EdModeWidget, SProceduralDungeonEdModeWidget, SharedThis(this));
|
||||
FModeToolkit::Init(InitToolkitHost);
|
||||
}
|
||||
|
||||
void FProceduralDungeonEdModeToolkit::GetToolPaletteNames(TArray<FName>& InPaletteName) const
|
||||
{
|
||||
InPaletteName.Add(FName("DefaultPalette"));
|
||||
}
|
||||
|
||||
FText FProceduralDungeonEdModeToolkit::GetToolPaletteDisplayName(FName PaletteName) const
|
||||
{
|
||||
if (PaletteName == FName("DefaultPalette"))
|
||||
{
|
||||
return LOCTEXT("Mode.Default", "Default");
|
||||
}
|
||||
return FText();
|
||||
}
|
||||
|
||||
void FProceduralDungeonEdModeToolkit::BuildToolPalette(FName Palette, FToolBarBuilder& ToolbarBuilder)
|
||||
{
|
||||
auto CommandList = FProceduralDungeonEditorCommands::Get();
|
||||
|
||||
// DoorType property from Settings
|
||||
UProceduralDungeonEditorObject* EditorSettings = GetDungeonEditorMode()->Settings;
|
||||
FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
|
||||
FSinglePropertyParams Params;
|
||||
#if !COMPATIBILITY
|
||||
Params.bHideAssetThumbnail = true;
|
||||
#endif
|
||||
Params.NamePlacement = EPropertyNamePlacement::Hidden;
|
||||
TSharedPtr<ISinglePropertyView> SinglePropView = PropertyEditorModule.CreateSingleProperty(EditorSettings, "DoorType", Params);
|
||||
|
||||
TSharedPtr<SWidget> Widget =
|
||||
SNew(SHorizontalBox)
|
||||
.IsEnabled(this, &FProceduralDungeonEdModeToolkit::IsDoorTypeEnabled)
|
||||
+ SHorizontalBox::Slot()
|
||||
.VAlign(EVerticalAlignment::VAlign_Center)
|
||||
.AutoWidth()
|
||||
[
|
||||
SinglePropView.ToSharedRef()
|
||||
];
|
||||
|
||||
ToolbarBuilder.BeginSection("Default");
|
||||
ToolbarBuilder.AddToolBarButton(CommandList.SizeTool);
|
||||
ToolbarBuilder.AddSeparator();
|
||||
ToolbarBuilder.AddToolBarButton(CommandList.DoorTool);
|
||||
ToolbarBuilder.AddWidget(Widget.ToSharedRef());
|
||||
ToolbarBuilder.EndSection();
|
||||
}
|
||||
|
||||
FEdMode* FProceduralDungeonEdModeToolkit::GetEditorMode() const
|
||||
{
|
||||
return GLevelEditorModeTools().GetActiveMode(FProceduralDungeonEdMode::EM_ProceduralDungeon);
|
||||
}
|
||||
|
||||
FProceduralDungeonEdMode* FProceduralDungeonEdModeToolkit::GetDungeonEditorMode() const
|
||||
{
|
||||
return (FProceduralDungeonEdMode*)GetEditorMode();
|
||||
}
|
||||
|
||||
TSharedPtr<SWidget> FProceduralDungeonEdModeToolkit::GetInlineContent() const
|
||||
{
|
||||
return EdModeWidget;
|
||||
}
|
||||
|
||||
void FProceduralDungeonEdModeToolkit::OnChangeTool(FName ToolName) const
|
||||
{
|
||||
FProceduralDungeonEdMode* EdMode = GetDungeonEditorMode();
|
||||
if (!EdMode)
|
||||
{
|
||||
DungeonEd_LogError("Editor Mode is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
DungeonEd_LogInfo("Change Tool to '%s'", *ToolName.ToString());
|
||||
EdMode->SetActiveTool(ToolName);
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEdModeToolkit::IsToolEnabled(FName ToolName) const
|
||||
{
|
||||
FProceduralDungeonEdMode* EdMode = GetDungeonEditorMode();
|
||||
return EdMode && EdMode->IsToolEnabled(ToolName);
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEdModeToolkit::IsToolActive(FName ToolName) const
|
||||
{
|
||||
FProceduralDungeonEdMode* EdMode = GetDungeonEditorMode();
|
||||
if (EdMode)
|
||||
{
|
||||
FProceduralDungeonEditorTool* Tool = nullptr;
|
||||
if (EdMode->GetTool(ToolName, Tool))
|
||||
return EdMode->GetActiveTool() == Tool;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEdModeToolkit::IsDoorTypeEnabled() const
|
||||
{
|
||||
return IsToolEnabled("Tool_Door");
|
||||
}
|
||||
|
||||
void FProceduralDungeonEdModeToolkit::OnLevelChanged()
|
||||
{
|
||||
EdModeWidget->OnLevelChanged();
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Toolkits/BaseToolkit.h"
|
||||
|
||||
class FProceduralDungeonEdModeToolkit : public FModeToolkit
|
||||
{
|
||||
public:
|
||||
/** FModeToolkit interface */
|
||||
virtual void Init(const TSharedPtr<class IToolkitHost>& InitToolkitHost) override;
|
||||
virtual void GetToolPaletteNames(TArray<FName>& InPaletteName) const override;
|
||||
virtual FText GetToolPaletteDisplayName(FName PaletteName) const override;
|
||||
virtual void BuildToolPalette(FName Palette, class FToolBarBuilder& ToolbarBuilder) override;
|
||||
|
||||
/** IToolkit interface */
|
||||
virtual FName GetToolkitFName() const override { return FName("ProceduralDungeonEdMode"); }
|
||||
virtual FText GetBaseToolkitName() const override { return NSLOCTEXT("ProceduralDungeonEdModeToolkit", "DisplayName", "ProceduralDungeonEdMode Tool"); }
|
||||
virtual FEdMode* GetEditorMode() const override;
|
||||
virtual TSharedPtr<class SWidget> GetInlineContent() const override;
|
||||
|
||||
class FProceduralDungeonEdMode* GetDungeonEditorMode() const;
|
||||
|
||||
void OnChangeTool(FName ToolName) const;
|
||||
bool IsToolEnabled(FName ToolName) const;
|
||||
bool IsToolActive(FName ToolName) const;
|
||||
bool IsDoorTypeEnabled() const;
|
||||
void OnLevelChanged();
|
||||
|
||||
private:
|
||||
TSharedPtr<class SProceduralDungeonEdModeWidget> EdModeWidget;
|
||||
};
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "ProceduralDungeonEditorCommands.h"
|
||||
#include "Tools/ProceduralDungeonEditorTool_Size.h"
|
||||
#include "Tools/ProceduralDungeonEditorTool_Door.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "ProceduralDungeonEditorCommands"
|
||||
|
||||
FName FProceduralDungeonEditorCommands::ProceduralDungeonContext = TEXT("ProceduralDungeonEditor");
|
||||
|
||||
FProceduralDungeonEditorCommands::FProceduralDungeonEditorCommands()
|
||||
: TCommands<FProceduralDungeonEditorCommands>
|
||||
(
|
||||
FProceduralDungeonEditorCommands::ProceduralDungeonContext, // Context name for fast lookup
|
||||
NSLOCTEXT("Contexts", "ProceduralDungeonEditor", "Procedural Dungeon Editor"), // Localized context name for displaying
|
||||
NAME_None, // Parent
|
||||
FName("ProceduralDungeonStyle") // Icon Style Set
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorCommands::RegisterCommands()
|
||||
{
|
||||
UI_COMMAND(SizeTool, "Size", "Change room size by dragging points in the viewport.", EUserInterfaceActionType::RadioButton, FInputChord());
|
||||
NameToCommandMap.Add("Tool_Size", SizeTool);
|
||||
|
||||
UI_COMMAND(DoorTool, "Door", "Add doors easily by clicking in the viewport.", EUserInterfaceActionType::RadioButton, FInputChord());
|
||||
NameToCommandMap.Add("Tool_Door", DoorTool);
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Framework/Commands/Commands.h"
|
||||
#include "EditorStyleSet.h"
|
||||
|
||||
class FProceduralDungeonEditorCommands : public TCommands<FProceduralDungeonEditorCommands>
|
||||
{
|
||||
public:
|
||||
FProceduralDungeonEditorCommands();
|
||||
|
||||
virtual void RegisterCommands() override;
|
||||
|
||||
public:
|
||||
static FName ProceduralDungeonContext;
|
||||
|
||||
// Tools
|
||||
TSharedPtr<FUICommandInfo> SizeTool = nullptr;
|
||||
TSharedPtr<FUICommandInfo> DoorTool = nullptr;
|
||||
|
||||
// Map
|
||||
TMap<FName, TSharedPtr<FUICommandInfo>> NameToCommandMap;
|
||||
};
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "ProceduralDungeonEditorObject.h"
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/ObjectMacros.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "ProceduralDungeonEditorObject.generated.h"
|
||||
|
||||
UCLASS(MinimalApi)
|
||||
class UProceduralDungeonEditorObject : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, Category = "Procedural Dungeon Editor")
|
||||
class UDoorType* DoorType {nullptr};
|
||||
};
|
||||
+650
@@ -0,0 +1,650 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "SProceduralDungeonEdModeWidget.h"
|
||||
#include "EditorModeManager.h"
|
||||
#include "EditorStyleSet.h"
|
||||
#include "ISinglePropertyView.h"
|
||||
#include "Subsystems/AssetEditorSubsystem.h"
|
||||
#include "Widgets/Layout/SScrollBox.h"
|
||||
#include "Widgets/Notifications/SErrorText.h"
|
||||
#include "Widgets/Text/STextBlock.h"
|
||||
#include "Widgets/SOverlay.h"
|
||||
#include "Widgets/Input/SSpinBox.h"
|
||||
#include "FileHelpers.h"
|
||||
#include "Engine/LevelScriptBlueprint.h"
|
||||
#include "Kismet2/KismetEditorUtilities.h"
|
||||
#include "Misc/EngineVersionComparison.h"
|
||||
#include "GameFramework/Volume.h"
|
||||
#include "Builders/CubeBuilder.h"
|
||||
#include "Engine/Selection.h"
|
||||
#include "ProceduralDungeonEdLog.h"
|
||||
#include "ProceduralDungeonEditor.h"
|
||||
#include "ProceduralDungeonEdMode.h"
|
||||
#include "ProceduralDungeonEdModeToolkit.h"
|
||||
#include "ProceduralDungeonEditorSettings.h"
|
||||
#include "Tools/ProceduralDungeonEditorTool.h"
|
||||
#include "Room.h" // TODO: remove the need to include Room.h when including RoomLevel.h
|
||||
#include "RoomLevel.h"
|
||||
#include "RoomData.h"
|
||||
|
||||
#if UE_VERSION_OLDER_THAN(5, 1, 0)
|
||||
using StyleProvider = FEditorStyle;
|
||||
#else
|
||||
using StyleProvider = FAppStyle;
|
||||
#endif
|
||||
|
||||
void SProceduralDungeonEdModeWidget::Construct(const FArguments& InArgs, TSharedRef<FProceduralDungeonEdModeToolkit> InParentToolkit)
|
||||
{
|
||||
ParentToolkit = InParentToolkit;
|
||||
FProceduralDungeonEdMode* EdMode = InParentToolkit->GetDungeonEditorMode();
|
||||
FText LevelName = FText::FromString(GetNameSafe(EdMode->GetWorld()));
|
||||
|
||||
FSlateFontInfo TitleFont = StyleProvider::GetFontStyle("DetailsView.CategoryFontStyle");
|
||||
TitleFont.Size = 24;
|
||||
|
||||
FSlateFontInfo SubTitleFont = StyleProvider::GetFontStyle("DetailsView.CategoryFontStyle");
|
||||
SubTitleFont.Size = 16;
|
||||
|
||||
const UProceduralDungeonEditorSettings* EditorSettings = GetDefault<UProceduralDungeonEditorSettings>();
|
||||
VolumeMargins = EditorSettings->DefaultMargins;
|
||||
|
||||
TSharedPtr<SScrollBox> DataScrollBox = nullptr;
|
||||
|
||||
ChildSlot
|
||||
[
|
||||
SNew(SVerticalBox)
|
||||
+ SVerticalBox::Slot()
|
||||
.VAlign(VAlign_Top)
|
||||
.Padding(5.f)
|
||||
.AutoHeight()
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(LevelName)
|
||||
.Justification(ETextJustify::Center)
|
||||
.AutoWrapText(true)
|
||||
.WrappingPolicy(ETextWrappingPolicy::AllowPerCharacterWrapping)
|
||||
.Font(TitleFont)
|
||||
]
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(5.0f)
|
||||
[
|
||||
SAssignNew(LevelPropertyContainer, SBorder)
|
||||
.Visibility(this, &SProceduralDungeonEdModeWidget::ShowDetails)
|
||||
.BorderBackgroundColor(FLinearColor(0.0f, 0.0f, 0.0f, 0.0f))
|
||||
]
|
||||
+ SVerticalBox::Slot()
|
||||
.Padding(5.0f)
|
||||
.AutoHeight()
|
||||
[
|
||||
SAssignNew(Error, SErrorText)
|
||||
]
|
||||
+ SVerticalBox::Slot()
|
||||
.Padding(5.0f)
|
||||
[
|
||||
SNew(SBorder)
|
||||
.BorderImage(StyleProvider::GetBrush("DetailsView.CollapsedCategory"))
|
||||
.BorderBackgroundColor(FLinearColor(0.2f, 0.2f, 0.2f, 1.0f))
|
||||
.Padding(5.0f)
|
||||
.Visibility(this, &SProceduralDungeonEdModeWidget::ShowDataDetails)
|
||||
[
|
||||
SNew(SVerticalBox)
|
||||
+ SVerticalBox::Slot()
|
||||
.VAlign(VAlign_Top)
|
||||
.AutoHeight()
|
||||
[
|
||||
SNew(SOverlay)
|
||||
+ SOverlay::Slot()
|
||||
.HAlign(EHorizontalAlignment::HAlign_Fill)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(this, &SProceduralDungeonEdModeWidget::GetDataAssetName)
|
||||
.Justification(ETextJustify::Center)
|
||||
.Font(SubTitleFont)
|
||||
]
|
||||
+ SOverlay::Slot()
|
||||
.HAlign(EHorizontalAlignment::HAlign_Left)
|
||||
.VAlign(EVerticalAlignment::VAlign_Center)
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(FText::FromString(TEXT("Edit")))
|
||||
.OnClicked(this, &SProceduralDungeonEdModeWidget::EditData)
|
||||
.IsEnabled(this, &SProceduralDungeonEdModeWidget::IsValidRoomData)
|
||||
.HAlign(EHorizontalAlignment::HAlign_Center)
|
||||
]
|
||||
+ SOverlay::Slot()
|
||||
.HAlign(EHorizontalAlignment::HAlign_Right)
|
||||
.VAlign(EVerticalAlignment::VAlign_Center)
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(FText::FromString(TEXT("Save")))
|
||||
.OnClicked(this, &SProceduralDungeonEdModeWidget::SaveData)
|
||||
.IsEnabled(this, &SProceduralDungeonEdModeWidget::IsDataDirty)
|
||||
.ButtonColorAndOpacity(this, &SProceduralDungeonEdModeWidget::GetSaveButtonColor)
|
||||
.HAlign(EHorizontalAlignment::HAlign_Center)
|
||||
]
|
||||
]
|
||||
+ SVerticalBox::Slot()
|
||||
.FillHeight(1.0f)
|
||||
.Padding(0.0f, 5.0f, 0.0f, 0.0f)
|
||||
[
|
||||
SAssignNew(DataScrollBox, SScrollBox)
|
||||
//.IsEnabled(this, &SProceduralDungeonEdModeWidget::MatchingDataLevel)
|
||||
]
|
||||
]
|
||||
]
|
||||
+ SVerticalBox::Slot()
|
||||
.Padding(5.0f)
|
||||
.AutoHeight()
|
||||
[
|
||||
SNew(SVerticalBox)
|
||||
.Visibility(this, &SProceduralDungeonEdModeWidget::ShowDataDetails)
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.HAlign(EHorizontalAlignment::HAlign_Center)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("Utilities")))
|
||||
.Font(SubTitleFont)
|
||||
]
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0.0f, 5.0f, 0.0f, 0.0f)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(FText::FromString(TEXT("Update Selected Volumes")))
|
||||
.IsEnabled_Lambda([this]() { return SelectedVolumeCount > 0; })
|
||||
.OnClicked(this, &SProceduralDungeonEdModeWidget::UpdateSelectedVolumes)
|
||||
.ToolTipText(FText::FromString(TEXT("Selected volumes will be positioned and sized on the room's bounds (defined in data asset).\nAn optional margin can be set.")))
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.Padding(20.0f, 0.0f, 0.0f, 0.0f)
|
||||
.VAlign(EVerticalAlignment::VAlign_Center)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("Margins")))
|
||||
.ToolTipText(FText::FromString(TEXT("The amount (in Unreal Unit) to extend the volumes on each side of the room bounds (can be negative).")))
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.Padding(10.0f, 0.0f, 5.0f, 0.0f)
|
||||
.VAlign(EVerticalAlignment::VAlign_Center)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("X")))
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.FillWidth(1)
|
||||
[
|
||||
SNew(SSpinBox<float>)
|
||||
.Value(VolumeMargins.XAxis.X)
|
||||
.OnValueChanged_Lambda([this](float Value) {VolumeMargins.XAxis.X = Value; })
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.FillWidth(1)
|
||||
[
|
||||
SNew(SSpinBox<float>)
|
||||
.Value(VolumeMargins.XAxis.Y)
|
||||
.OnValueChanged_Lambda([this](float Value) {VolumeMargins.XAxis.Y = Value; })
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.Padding(10.0f, 0.0f, 5.0f, 0.0f)
|
||||
.VAlign(EVerticalAlignment::VAlign_Center)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("Y")))
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.FillWidth(1)
|
||||
[
|
||||
SNew(SSpinBox<float>)
|
||||
.Value(VolumeMargins.YAxis.X)
|
||||
.OnValueChanged_Lambda([this](float Value) {VolumeMargins.YAxis.X = Value; })
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.FillWidth(1)
|
||||
[
|
||||
SNew(SSpinBox<float>)
|
||||
.Value(VolumeMargins.YAxis.Y)
|
||||
.OnValueChanged_Lambda([this](float Value) {VolumeMargins.YAxis.Y = Value; })
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.Padding(10.0f, 0.0f, 5.0f, 0.0f)
|
||||
.VAlign(EVerticalAlignment::VAlign_Center)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("Z")))
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.FillWidth(1)
|
||||
[
|
||||
SNew(SSpinBox<float>)
|
||||
.Value(VolumeMargins.ZAxis.X)
|
||||
.OnValueChanged_Lambda([this](float Value) {VolumeMargins.ZAxis.X = Value; })
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.FillWidth(1)
|
||||
[
|
||||
SNew(SSpinBox<float>)
|
||||
.Value(VolumeMargins.ZAxis.Y)
|
||||
.OnValueChanged_Lambda([this](float Value) {VolumeMargins.ZAxis.Y = Value; })
|
||||
]
|
||||
]
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
.Padding(0.0f, 5.0f, 0.0f, 0.0f)
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(FText::FromString(TEXT("Remove All Invalid Doors")))
|
||||
//.IsEnabled_Lambda([this]() { return SelectedVolumeCount > 0; })
|
||||
.OnClicked(this, &SProceduralDungeonEdModeWidget::RemoveInvalidDoors)
|
||||
.ToolTipText(FText::FromString(TEXT("All invalid doors (drawn in orange) will be removed.")))
|
||||
]
|
||||
]
|
||||
]
|
||||
+ SVerticalBox::Slot()
|
||||
.Padding(5.0f)
|
||||
.AutoHeight()
|
||||
[
|
||||
SNew(SBorder)
|
||||
.HAlign(EHorizontalAlignment::HAlign_Center)
|
||||
.Visibility(this, &SProceduralDungeonEdModeWidget::ShowNote)
|
||||
.BorderBackgroundColor(FLinearColor(0.0f, 0.0f, 0.0f, 0.0f))
|
||||
.Padding(0.0f)
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(FText::FromString(TEXT("Reparent Level Blueprint")))
|
||||
.OnClicked(this, &SProceduralDungeonEdModeWidget::ReparentLevelActor)
|
||||
.ButtonColorAndOpacity(this, &SProceduralDungeonEdModeWidget::GetReparentButtonColor)
|
||||
.HAlign(EHorizontalAlignment::HAlign_Center)
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
|
||||
|
||||
// RoomData details view
|
||||
FDetailsViewArgs DetailsViewArgs;
|
||||
DetailsViewArgs.bAllowSearch = false;
|
||||
DetailsViewArgs.bLockable = false;
|
||||
DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea;
|
||||
|
||||
DataContentWidget = PropertyEditorModule.CreateDetailView(DetailsViewArgs);
|
||||
DataContentWidget->OnFinishedChangingProperties().AddLambda([this](const FPropertyChangedEvent& Event) { UpdateErrorText(); });
|
||||
DataScrollBox->AddSlot()
|
||||
[
|
||||
DataContentWidget.ToSharedRef()
|
||||
];
|
||||
|
||||
OnLevelChanged();
|
||||
|
||||
RegisterSelectionDelegate(true);
|
||||
OnSelectedActorsChanged(nullptr);
|
||||
}
|
||||
|
||||
SProceduralDungeonEdModeWidget::~SProceduralDungeonEdModeWidget()
|
||||
{
|
||||
RegisterSelectionDelegate(false);
|
||||
ResetCachedData();
|
||||
ResetCachedLevel();
|
||||
}
|
||||
|
||||
void SProceduralDungeonEdModeWidget::OnLevelChanged()
|
||||
{
|
||||
FProceduralDungeonEdMode* EdMode = GetEditorMode();
|
||||
ResetCachedLevel();
|
||||
if (!IsValidRoomLevel(EdMode, &CachedLevel))
|
||||
{
|
||||
UpdateErrorText();
|
||||
return;
|
||||
}
|
||||
|
||||
DungeonEd_LogInfo("Slate Editor Level: %s", *GetNameSafe(CachedLevel.Get()));
|
||||
|
||||
LevelDelegateHandle = CachedLevel->OnPropertiesChanged.AddLambda([this](ARoomLevel* RoomLevel) { OnDataAssetChanged(); });
|
||||
|
||||
// RoomLevel Data property
|
||||
FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
|
||||
TSharedPtr<ISinglePropertyView> SinglePropView = PropertyEditorModule.CreateSingleProperty(CachedLevel.Get(), "Data", {});
|
||||
LevelPropertyContainer->SetContent(SinglePropView.ToSharedRef());
|
||||
|
||||
OnDataAssetChanged();
|
||||
}
|
||||
|
||||
void SProceduralDungeonEdModeWidget::OnDataAssetChanged()
|
||||
{
|
||||
auto EdMode = GetEditorMode();
|
||||
ResetCachedData();
|
||||
if (IsValidRoomData(EdMode, &CachedData))
|
||||
{
|
||||
DataContentWidget->SetObject(CachedData.Get());
|
||||
if (CachedData->Level.IsNull())
|
||||
{
|
||||
CachedData->Modify();
|
||||
CachedData->Level = EdMode->GetWorld();
|
||||
DungeonEd_LogInfo("Room Data's Level asset filled with current editor's Level.");
|
||||
}
|
||||
DataDelegateHandle = CachedData->OnPropertiesChanged.AddLambda([this](URoomData* Data)
|
||||
{
|
||||
UpdateErrorText();
|
||||
|
||||
auto EdMode = GetEditorMode();
|
||||
if (EdMode)
|
||||
{
|
||||
FProceduralDungeonEditorTool* ActiveTool = EdMode->GetActiveTool();
|
||||
if (ActiveTool)
|
||||
ActiveTool->OnDataPropertiesChanged(CachedData.Get());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
UpdateErrorText();
|
||||
|
||||
EdMode->SetDefaultTool();
|
||||
|
||||
FProceduralDungeonEditorTool* ActiveTool = EdMode->GetActiveTool();
|
||||
if (ActiveTool)
|
||||
ActiveTool->OnDataChanged(CachedData.Get());
|
||||
}
|
||||
|
||||
FReply SProceduralDungeonEdModeWidget::ReparentLevelActor()
|
||||
{
|
||||
auto EdMode = GetEditorMode();
|
||||
auto World = EdMode->GetWorld();
|
||||
ULevelScriptBlueprint* LevelBlueprint = World->PersistentLevel->GetLevelScriptBlueprint();
|
||||
if (!IsValid(LevelBlueprint))
|
||||
{
|
||||
DungeonEd_LogError("ERROR: Can't Reparent Level Blueprint for an unknown reason.");
|
||||
return FReply::Unhandled();
|
||||
}
|
||||
|
||||
LevelBlueprint->ParentClass = ARoomLevel::StaticClass();
|
||||
FKismetEditorUtilities::CompileBlueprint(LevelBlueprint);
|
||||
|
||||
DungeonEd_LogInfo("Level Blueprint '%s' successfully reparented!", *LevelBlueprint->GetName());
|
||||
|
||||
EdMode->UpdateLevelBlueprint();
|
||||
|
||||
return FReply::Unhandled();
|
||||
}
|
||||
|
||||
FReply SProceduralDungeonEdModeWidget::EditData()
|
||||
{
|
||||
UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
|
||||
if (!IsValid(AssetEditorSubsystem))
|
||||
return FReply::Unhandled();
|
||||
|
||||
TWeakObjectPtr<URoomData> Data;
|
||||
if (!IsValidRoomData(nullptr, &Data))
|
||||
return FReply::Unhandled();
|
||||
|
||||
AssetEditorSubsystem->OpenEditorForAsset(Data.Get());
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FReply SProceduralDungeonEdModeWidget::SaveData()
|
||||
{
|
||||
TWeakObjectPtr<URoomData> Data;
|
||||
if (!IsValidRoomData(nullptr, &Data))
|
||||
return FReply::Unhandled();
|
||||
|
||||
auto Result = FEditorFileUtils::PromptForCheckoutAndSave({Data->GetPackage()}, /*bCheckDirty = */true, /*bPromptToSave = */false);
|
||||
if (Result == FEditorFileUtils::EPromptReturnCode::PR_Success)
|
||||
DungeonEd_LogInfo("Successfully Saved Data Asset: '%s'", *GetNameSafe(Data.Get()));
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FReply SProceduralDungeonEdModeWidget::UpdateSelectedVolumes()
|
||||
{
|
||||
DungeonEd_LogInfo("Update Selected Volumes.");
|
||||
|
||||
auto EdMode = GetEditorMode();
|
||||
TWeakObjectPtr<ARoomLevel> Level;
|
||||
TWeakObjectPtr<URoomData> Data;
|
||||
if (!IsValidRoomData(EdMode, &Data, &Level))
|
||||
{
|
||||
DungeonEd_LogError("Can't update selected volumes: RoomData is not valid.");
|
||||
return FReply::Unhandled();
|
||||
}
|
||||
|
||||
FBoxCenterAndExtent RoomBounds = Data->GetBounds();
|
||||
RoomBounds = VolumeMargins.Apply(RoomBounds);
|
||||
|
||||
GEditor->BeginTransaction(FText::FromString(TEXT("Update Selected Volumes")));
|
||||
for (auto It = GEditor->GetSelectedActorIterator(); It; ++It)
|
||||
{
|
||||
AVolume* Volume = Cast<AVolume>(*It);
|
||||
if (!IsValid(Volume))
|
||||
continue;
|
||||
|
||||
UCubeBuilder* CubeBrush = Cast<UCubeBuilder>(Volume->BrushBuilder);
|
||||
if (!IsValid(CubeBrush))
|
||||
{
|
||||
DungeonEd_LogWarning("Volume's brush is not a cube. Ignoring this volume.");
|
||||
continue;
|
||||
}
|
||||
|
||||
DungeonEd_LogInfo("Updating volume: '%s'", *Volume->GetName());
|
||||
|
||||
Volume->Modify();
|
||||
Volume->SetActorLocationAndRotation(RoomBounds.Center, FQuat::Identity);
|
||||
|
||||
CubeBrush->Modify();
|
||||
CubeBrush->X = 2.0f * RoomBounds.Extent.X;
|
||||
CubeBrush->Y = 2.0f * RoomBounds.Extent.Y;
|
||||
CubeBrush->Z = 2.0f * RoomBounds.Extent.Z;
|
||||
|
||||
// Rebuild volume after changing its builder values
|
||||
CubeBrush->Build(Volume->GetWorld(), Volume);
|
||||
}
|
||||
GEditor->EndTransaction();
|
||||
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FReply SProceduralDungeonEdModeWidget::RemoveInvalidDoors()
|
||||
{
|
||||
TWeakObjectPtr<URoomData> Data;
|
||||
if (!IsValidRoomData(nullptr, &Data))
|
||||
return FReply::Unhandled();
|
||||
|
||||
GEditor->BeginTransaction(FText::FromString(TEXT("Remove Invalid Doors")));
|
||||
Data->Modify();
|
||||
for (int i = Data->Doors.Num() - 1; i >= 0; --i)
|
||||
{
|
||||
if (!Data->IsDoorValid(i) || Data->IsDoorDuplicate(i))
|
||||
Data->Doors.RemoveAt(i);
|
||||
}
|
||||
GEditor->EndTransaction();
|
||||
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
FSlateColor SProceduralDungeonEdModeWidget::GetSaveButtonColor() const
|
||||
{
|
||||
const FLinearColor& Default = FLinearColor::White;
|
||||
const FLinearColor& Highlight = FLinearColor::Green;
|
||||
return IsDataDirty() ? GetHighlightButtonColor(Highlight, Default) : Default;
|
||||
}
|
||||
|
||||
FSlateColor SProceduralDungeonEdModeWidget::GetReparentButtonColor() const
|
||||
{
|
||||
return GetHighlightButtonColor(FLinearColor::Green);
|
||||
}
|
||||
|
||||
void SProceduralDungeonEdModeWidget::UpdateErrorText()
|
||||
{
|
||||
auto EdMode = GetEditorMode();
|
||||
checkf(EdMode, TEXT("EdMode is Invalid in UpdateErrorText"));
|
||||
if (!IsValidRoomLevel(EdMode))
|
||||
Error->SetError(TEXT("Persistent Level is not a Room Level."));
|
||||
else if (!IsValidRoomData(EdMode))
|
||||
Error->SetError(TEXT("Room Level has no Room Data set."));
|
||||
else if (!MatchingDataLevel(EdMode))
|
||||
Error->SetError(TEXT("Level's Data and Data's Level do not match."));
|
||||
else
|
||||
Error->SetError(FText::GetEmpty());
|
||||
}
|
||||
|
||||
void SProceduralDungeonEdModeWidget::ResetCachedData()
|
||||
{
|
||||
if (!CachedData.IsValid())
|
||||
return;
|
||||
CachedData->OnPropertiesChanged.Remove(DataDelegateHandle);
|
||||
DataDelegateHandle.Reset();
|
||||
CachedData.Reset();
|
||||
}
|
||||
|
||||
void SProceduralDungeonEdModeWidget::ResetCachedLevel()
|
||||
{
|
||||
if (!CachedLevel.IsValid())
|
||||
return;
|
||||
CachedLevel->OnPropertiesChanged.Remove(LevelDelegateHandle);
|
||||
LevelDelegateHandle.Reset();
|
||||
CachedLevel.Reset();
|
||||
}
|
||||
|
||||
FProceduralDungeonEdMode* SProceduralDungeonEdModeWidget::GetEditorMode() const
|
||||
{
|
||||
checkf(ParentToolkit.IsValid(), TEXT("ParentToolkit is invalid. This should never happen. There is a leakage somewhere."));
|
||||
return ParentToolkit.Pin()->GetDungeonEditorMode();
|
||||
}
|
||||
|
||||
void SProceduralDungeonEdModeWidget::RegisterSelectionDelegate(bool Register)
|
||||
{
|
||||
USelection* SelectedActors = GEditor->GetSelectedActors();
|
||||
checkf(IsValid(SelectedActors), TEXT("Editor Actor Selection is not valid!"));
|
||||
|
||||
if (Register)
|
||||
{
|
||||
if (SelectionDelegateHandle.IsValid())
|
||||
{
|
||||
DungeonEd_LogWarning("Can't register SelectionChanged callback: callback is already registered.");
|
||||
}
|
||||
else
|
||||
{
|
||||
DungeonEd_LogInfo("Register SelectionChanged callback.");
|
||||
SelectionDelegateHandle = SelectedActors->SelectionChangedEvent.AddRaw(this, &SProceduralDungeonEdModeWidget::OnSelectedActorsChanged);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (SelectionDelegateHandle.IsValid())
|
||||
{
|
||||
DungeonEd_LogInfo("Unregister SelectionChanged callback.");
|
||||
SelectedActors->SelectionChangedEvent.Remove(SelectionDelegateHandle);
|
||||
SelectionDelegateHandle.Reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
DungeonEd_LogWarning("Can't unregister SelectionChanged callback: callback is not registered.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SProceduralDungeonEdModeWidget::OnSelectedActorsChanged(UObject* NewSelectedObject)
|
||||
{
|
||||
USelection* SelectedActors = GEditor->GetSelectedActors();
|
||||
SelectedVolumeCount = SelectedActors->CountSelections<AVolume>();
|
||||
}
|
||||
|
||||
bool SProceduralDungeonEdModeWidget::IsValidRoomLevel(FProceduralDungeonEdMode* EdMode, TWeakObjectPtr<ARoomLevel>* OutLevel) const
|
||||
{
|
||||
if (!EdMode)
|
||||
EdMode = GetEditorMode();
|
||||
|
||||
auto Level = EdMode->GetLevel();
|
||||
if (OutLevel)
|
||||
*OutLevel = Level;
|
||||
|
||||
return Level.IsValid();
|
||||
}
|
||||
|
||||
bool SProceduralDungeonEdModeWidget::IsValidRoomData(FProceduralDungeonEdMode* EdMode, TWeakObjectPtr<URoomData>* OutData, TWeakObjectPtr<ARoomLevel>* OutLevel) const
|
||||
{
|
||||
if (!EdMode)
|
||||
EdMode = GetEditorMode();
|
||||
|
||||
TWeakObjectPtr<ARoomLevel> Level;
|
||||
if (!IsValidRoomLevel(EdMode, &Level))
|
||||
return false;
|
||||
|
||||
if (OutLevel)
|
||||
*OutLevel = Level;
|
||||
|
||||
if (OutData)
|
||||
*OutData = Level->Data;
|
||||
|
||||
return IsValid(Level->Data);
|
||||
}
|
||||
|
||||
bool SProceduralDungeonEdModeWidget::MatchingDataLevel(FProceduralDungeonEdMode* EdMode) const
|
||||
{
|
||||
if (!EdMode)
|
||||
EdMode = GetEditorMode();
|
||||
|
||||
TWeakObjectPtr<URoomData> Data;
|
||||
if (!IsValidRoomData(EdMode, &Data))
|
||||
return false;
|
||||
|
||||
return Data->Level.GetUniqueID() == EdMode->GetWorld()->GetPathName();
|
||||
}
|
||||
|
||||
bool SProceduralDungeonEdModeWidget::IsDataDirty(FProceduralDungeonEdMode* EdMode) const
|
||||
{
|
||||
TWeakObjectPtr<URoomData> Data;
|
||||
if (!IsValidRoomData(EdMode, &Data))
|
||||
return false;
|
||||
|
||||
return Data->GetPackage()->IsDirty();
|
||||
}
|
||||
|
||||
EVisibility SProceduralDungeonEdModeWidget::ShowDetails() const
|
||||
{
|
||||
return IsValidRoomLevel() ? EVisibility::Visible : EVisibility::Collapsed;
|
||||
}
|
||||
|
||||
EVisibility SProceduralDungeonEdModeWidget::ShowDataDetails() const
|
||||
{
|
||||
return IsValidRoomData() ? EVisibility::Visible : EVisibility::Collapsed;
|
||||
}
|
||||
|
||||
EVisibility SProceduralDungeonEdModeWidget::ShowNote() const
|
||||
{
|
||||
return IsValidRoomLevel() ? EVisibility::Collapsed : EVisibility::Visible;
|
||||
}
|
||||
|
||||
FText SProceduralDungeonEdModeWidget::GetDataAssetName() const
|
||||
{
|
||||
auto EdMode = GetEditorMode();
|
||||
TWeakObjectPtr<URoomData> Data;
|
||||
if (!IsValidRoomData(EdMode, &Data))
|
||||
return FText::GetEmpty();
|
||||
|
||||
FString Dirty = IsDataDirty(EdMode) ? "*" : "";
|
||||
return FText::FromString(GetNameSafe(Data.Get()) + Dirty);
|
||||
}
|
||||
|
||||
FLinearColor SProceduralDungeonEdModeWidget::GetHighlightButtonColor(const FLinearColor& HighlightColor, const FLinearColor& NormalColor, float Speed)
|
||||
{
|
||||
uint32 ticks = FDateTime::Now().GetTicks(); // needs this line to avoid compiler optimization that prevent getting Now() each frame.
|
||||
float seconds = static_cast<float>(ticks) / ETimespan::TicksPerSecond;
|
||||
float t = FMath::Clamp(FMath::Cos(Speed * seconds), 0.0f, 1.0f);
|
||||
return FMath::Lerp(NormalColor, HighlightColor, t);
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Framework/Application/SlateApplication.h"
|
||||
#include "ProceduralDungeonEdTypes.h"
|
||||
|
||||
class ARoomLevel;
|
||||
class URoomData;
|
||||
class FProceduralDungeonEdMode;
|
||||
class FProceduralDungeonEdModeToolkit;
|
||||
template <typename> class SSpinBox;
|
||||
|
||||
class SProceduralDungeonEdModeWidget : public SCompoundWidget
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SProceduralDungeonEdModeWidget) {}
|
||||
SLATE_END_ARGS();
|
||||
|
||||
~SProceduralDungeonEdModeWidget();
|
||||
|
||||
void Construct(const FArguments& InArgs, TSharedRef<FProceduralDungeonEdModeToolkit> InParentToolkit);
|
||||
void OnLevelChanged();
|
||||
|
||||
protected:
|
||||
bool IsValidRoomLevel(FProceduralDungeonEdMode* EdMode = nullptr, TWeakObjectPtr<ARoomLevel>* OutLevel = nullptr) const;
|
||||
bool IsValidRoomData(FProceduralDungeonEdMode* EdMode, TWeakObjectPtr<URoomData>* OutData = nullptr, TWeakObjectPtr<ARoomLevel>* OutLevel = nullptr) const;
|
||||
bool IsValidRoomData() const { return IsValidRoomData(nullptr); }
|
||||
bool MatchingDataLevel(FProceduralDungeonEdMode* EdMode = nullptr) const;
|
||||
bool IsDataDirty(FProceduralDungeonEdMode* EdMode) const;
|
||||
bool IsDataDirty() const { return IsDataDirty(nullptr); }
|
||||
EVisibility ShowDetails() const;
|
||||
EVisibility ShowDataDetails() const;
|
||||
EVisibility ShowNote() const;
|
||||
FText GetDataAssetName() const;
|
||||
void OnDataAssetChanged();
|
||||
FReply ReparentLevelActor();
|
||||
FReply EditData();
|
||||
FReply SaveData();
|
||||
FReply UpdateSelectedVolumes();
|
||||
FReply RemoveInvalidDoors();
|
||||
FSlateColor GetSaveButtonColor() const;
|
||||
FSlateColor GetReparentButtonColor() const;
|
||||
void UpdateErrorText();
|
||||
|
||||
void ResetCachedData();
|
||||
void ResetCachedLevel();
|
||||
|
||||
FProceduralDungeonEdMode* GetEditorMode() const;
|
||||
void RegisterSelectionDelegate(bool Register);
|
||||
void OnSelectedActorsChanged(UObject* NewSelectedObject);
|
||||
|
||||
static FLinearColor GetHighlightButtonColor(const FLinearColor& HighlightColor, const FLinearColor& NormalColor = FLinearColor::White, float Speed = 3.0f);
|
||||
|
||||
private:
|
||||
TSharedPtr<class SErrorText> Error {nullptr};
|
||||
TSharedPtr<class IDetailsView> DataContentWidget {nullptr};
|
||||
TWeakPtr<FProceduralDungeonEdModeToolkit> ParentToolkit {nullptr};
|
||||
|
||||
FMargin3D VolumeMargins;
|
||||
|
||||
TSharedPtr<class SBorder> LevelPropertyContainer {nullptr};
|
||||
TWeakObjectPtr<URoomData> CachedData {nullptr};
|
||||
TWeakObjectPtr<ARoomLevel> CachedLevel {nullptr};
|
||||
FDelegateHandle DataDelegateHandle;
|
||||
FDelegateHandle LevelDelegateHandle;
|
||||
FDelegateHandle SelectionDelegateHandle;
|
||||
int32 SelectedVolumeCount {0};
|
||||
};
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// Copyright Benoit Pelletier 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "ProceduralDungeonEditorTool.h"
|
||||
#include "EditorMode/ProceduralDungeonEdMode.h"
|
||||
#include "RoomLevel.h"
|
||||
#include "RoomData.h"
|
||||
|
||||
URoomData* FProceduralDungeonEditorTool::GetRoomData() const
|
||||
{
|
||||
auto Level = EdMode->GetLevel();
|
||||
if (!Level.IsValid())
|
||||
return nullptr;
|
||||
|
||||
return Level.Get()->Data;
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "InputCoreTypes.h"
|
||||
#include "UObject/GCObject.h"
|
||||
#include "EdMode.h"
|
||||
#include "EditorUndoClient.h"
|
||||
#include "ProceduralDungeonEdTypes.h"
|
||||
|
||||
class FProceduralDungeonEdMode;
|
||||
|
||||
class FProceduralDungeonEditorTool : public FGCObject, public FSelfRegisteringEditorUndoClient
|
||||
{
|
||||
public:
|
||||
FProceduralDungeonEditorTool(FProceduralDungeonEdMode* InEdMode)
|
||||
: EdMode(InEdMode)
|
||||
{
|
||||
}
|
||||
|
||||
virtual const TCHAR* GetToolName() = 0;
|
||||
virtual FText GetDisplayName() = 0;
|
||||
virtual FText GetDisplayMessage() = 0;
|
||||
|
||||
virtual void EnterTool() = 0;
|
||||
virtual void ExitTool() = 0;
|
||||
virtual void Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI) {}
|
||||
virtual void Tick(FEditorViewportClient* ViewportClient, float DeltaTime) {}
|
||||
virtual bool MouseMove(FEditorViewportClient* ViewportClient, FViewport* Viewport, int32 MouseX, int32 MouseY) { return false; }
|
||||
virtual bool HandleClick(FEditorViewportClient* InViewportClient, HHitProxy* HitProxy, const FViewportClick& Click) { return false; }
|
||||
virtual bool InputKey(FEditorViewportClient* InViewportClient, FViewport* InViewport, FKey InKey, EInputEvent InEvent) { return false; }
|
||||
virtual bool InputAxis(FEditorViewportClient* InViewportClient, FViewport* Viewport, int32 ControllerId, FKey Key, float Delta, float DeltaTime) { return false; }
|
||||
virtual bool InputDelta(FEditorViewportClient* InViewportClient, FViewport* InViewport, FVector& InDrag, FRotator& InRot, FVector& InScale) { return false; }
|
||||
virtual bool UsesTransformWidget() const { return false; }
|
||||
virtual bool UsesTransformWidget(WidgetMode CheckMode) const { return false; }
|
||||
virtual FVector GetWidgetLocation() const { return FVector::ZeroVector; }
|
||||
virtual bool GetCursor(EMouseCursor::Type& OutCursor) const { return false; }
|
||||
|
||||
//~ Begin FGCObject Interface
|
||||
virtual void AddReferencedObjects(FReferenceCollector& Collector) override {}
|
||||
virtual FString GetReferencerName() const override
|
||||
{
|
||||
return TEXT("FProceduralDungeonEditorTool");
|
||||
}
|
||||
//~ End FGCObject Interface
|
||||
|
||||
virtual void OnLevelChanged(const class ARoomLevel* NewLevel) {}
|
||||
virtual void OnDataChanged(const class URoomData* NewData) {}
|
||||
virtual void OnDataPropertiesChanged(const class URoomData* Data) {}
|
||||
|
||||
class URoomData* GetRoomData() const;
|
||||
|
||||
protected:
|
||||
FProceduralDungeonEdMode* EdMode = nullptr;
|
||||
};
|
||||
+428
@@ -0,0 +1,428 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "ProceduralDungeonEditorTool_Door.h"
|
||||
#include "Components/BoxComponent.h"
|
||||
#include "EditorMode/ProceduralDungeonEdMode.h"
|
||||
#include "EditorMode/ProceduralDungeonEditorObject.h"
|
||||
#include "ProceduralDungeonEdLog.h"
|
||||
#include "ProceduralDungeonUtils.h"
|
||||
#include "Room.h"
|
||||
#include "RoomLevel.h"
|
||||
#include "RoomData.h"
|
||||
#include "Door.h"
|
||||
|
||||
bool AreDoorsOverlapping(const FDoorDef& DoorA, const FDoorDef& DoorB, const FVector RoomUnit)
|
||||
{
|
||||
// If not in same direction, they will never overlap.
|
||||
if (DoorA.Direction != DoorB.Direction)
|
||||
return false;
|
||||
|
||||
// If same direction and position, then will always overlap.
|
||||
if (DoorA.Position == DoorB.Position)
|
||||
return true;
|
||||
|
||||
const FBoxCenterAndExtent DoorBoundsA = DoorA.GetBounds(RoomUnit);
|
||||
const FBoxCenterAndExtent DoorBoundsB = DoorB.GetBounds(RoomUnit);
|
||||
return Intersect(DoorBoundsA, DoorBoundsB);
|
||||
}
|
||||
|
||||
bool IsPositionInside(const FDoorDef& Door, const FIntVector& Position, const FVector RoomUnit)
|
||||
{
|
||||
// If same position, then always inside.
|
||||
if (Door.Position == Position)
|
||||
return true;
|
||||
|
||||
const FBoxCenterAndExtent DoorBounds = Door.GetBounds(RoomUnit);
|
||||
const FVector LocalDoorExtent = DoorBounds.Extent / RoomUnit;
|
||||
const FVector RelativePosition = (FVector(Position) - (DoorBounds.Center / RoomUnit)).GetAbs();
|
||||
return RelativePosition.X <= LocalDoorExtent.X && RelativePosition.Y <= LocalDoorExtent.Y && RelativePosition.Z <= LocalDoorExtent.Z;
|
||||
}
|
||||
|
||||
bool IsDoorValid(const URoomData* Data, const FDoorDef& Door)
|
||||
{
|
||||
check(IsValid(Data));
|
||||
for (const FDoorDef& DoorDef : Data->Doors)
|
||||
{
|
||||
if (AreDoorsOverlapping(Door, DoorDef, Data->GetRoomUnit()))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Door::EnterTool()
|
||||
{
|
||||
DungeonEd_LogInfo("Enter Door Tool.");
|
||||
UpdateCollision();
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Door::ExitTool()
|
||||
{
|
||||
DungeonEd_LogInfo("Exit Door Tool.");
|
||||
DestroyCollision();
|
||||
CachedLevel.Reset();
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
void RenderBoundingBox(FPrimitiveDrawInterface* PDI, const FBoxCenterAndExtent& Box, const FColor& Color)
|
||||
{
|
||||
const FVector Center = Box.Center;
|
||||
const FVector Extent = Box.Extent;
|
||||
const FVector A = Center + FVector(Extent.X, Extent.Y, Extent.Z);
|
||||
const FVector B = Center + FVector(Extent.X, Extent.Y, -Extent.Z);
|
||||
const FVector C = Center + FVector(Extent.X, -Extent.Y, Extent.Z);
|
||||
const FVector D = Center + FVector(Extent.X, -Extent.Y, -Extent.Z);
|
||||
const FVector E = Center + FVector(-Extent.X, Extent.Y, Extent.Z);
|
||||
const FVector F = Center + FVector(-Extent.X, Extent.Y, -Extent.Z);
|
||||
const FVector G = Center + FVector(-Extent.X, -Extent.Y, Extent.Z);
|
||||
const FVector H = Center + FVector(-Extent.X, -Extent.Y, -Extent.Z);
|
||||
PDI->DrawLine(A, B, Color, SDPG_Foreground);
|
||||
PDI->DrawLine(A, C, Color, SDPG_Foreground);
|
||||
PDI->DrawLine(A, E, Color, SDPG_Foreground);
|
||||
PDI->DrawLine(B, D, Color, SDPG_Foreground);
|
||||
PDI->DrawLine(B, F, Color, SDPG_Foreground);
|
||||
PDI->DrawLine(C, D, Color, SDPG_Foreground);
|
||||
PDI->DrawLine(C, G, Color, SDPG_Foreground);
|
||||
PDI->DrawLine(D, H, Color, SDPG_Foreground);
|
||||
PDI->DrawLine(E, F, Color, SDPG_Foreground);
|
||||
PDI->DrawLine(E, G, Color, SDPG_Foreground);
|
||||
PDI->DrawLine(F, H, Color, SDPG_Foreground);
|
||||
PDI->DrawLine(G, H, Color, SDPG_Foreground);
|
||||
}
|
||||
|
||||
void RenderInterlinesBoundingBox(FPrimitiveDrawInterface* PDI, const FBoxMinAndMax& Box, const FColor& Color, const FVector& RoomUnit)
|
||||
{
|
||||
FIntVector Min, Max;
|
||||
IntVector::MinMax(Box.GetMin(), Box.GetMax(), Min, Max);
|
||||
|
||||
// Vertical Lines on X
|
||||
for (int32 i = Min.X + 1; i < Max.X; ++i)
|
||||
{
|
||||
FIntVector BottomA(i, Min.Y, Min.Z);
|
||||
FIntVector BottomB(i, Max.Y, Min.Z);
|
||||
FIntVector TopA(i, Min.Y, Max.Z);
|
||||
FIntVector TopB(i, Max.Y, Max.Z);
|
||||
PDI->DrawLine(Dungeon::ToWorldLocation(BottomA, RoomUnit), Dungeon::ToWorldLocation(TopA, RoomUnit), Color, SDPG_World);
|
||||
PDI->DrawLine(Dungeon::ToWorldLocation(BottomB, RoomUnit), Dungeon::ToWorldLocation(TopB, RoomUnit), Color, SDPG_World);
|
||||
}
|
||||
|
||||
// Vertical Lines on Y
|
||||
for (int32 i = Min.Y + 1; i < Max.Y; ++i)
|
||||
{
|
||||
FIntVector BottomA(Min.X, i, Min.Z);
|
||||
FIntVector BottomB(Max.X, i, Min.Z);
|
||||
FIntVector TopA(Min.X, i, Max.Z);
|
||||
FIntVector TopB(Max.X, i, Max.Z);
|
||||
PDI->DrawLine(Dungeon::ToWorldLocation(BottomA, RoomUnit), Dungeon::ToWorldLocation(TopA, RoomUnit), Color, SDPG_World);
|
||||
PDI->DrawLine(Dungeon::ToWorldLocation(BottomB, RoomUnit), Dungeon::ToWorldLocation(TopB, RoomUnit), Color, SDPG_World);
|
||||
}
|
||||
|
||||
// Horizontal Lines on X and Y
|
||||
for (int32 i = Min.Z + 1; i < Max.Z; ++i)
|
||||
{
|
||||
FIntVector A(Min.X, Min.Y, i);
|
||||
FIntVector B(Min.X, Max.Y, i);
|
||||
FIntVector C(Max.X, Max.Y, i);
|
||||
FIntVector D(Max.X, Min.Y, i);
|
||||
PDI->DrawLine(Dungeon::ToWorldLocation(A, RoomUnit), Dungeon::ToWorldLocation(B, RoomUnit), Color, SDPG_World);
|
||||
PDI->DrawLine(Dungeon::ToWorldLocation(B, RoomUnit), Dungeon::ToWorldLocation(C, RoomUnit), Color, SDPG_World);
|
||||
PDI->DrawLine(Dungeon::ToWorldLocation(C, RoomUnit), Dungeon::ToWorldLocation(D, RoomUnit), Color, SDPG_World);
|
||||
PDI->DrawLine(Dungeon::ToWorldLocation(D, RoomUnit), Dungeon::ToWorldLocation(A, RoomUnit), Color, SDPG_World);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Door::Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI)
|
||||
{
|
||||
FProceduralDungeonEditorTool::Render(View, Viewport, PDI);
|
||||
|
||||
const URoomData* Data = GetRoomData();
|
||||
if (!IsValid(Data))
|
||||
return;
|
||||
|
||||
const FColor LineColor(100, 20, 0);
|
||||
const FVector RoomUnit = Data->GetRoomUnit();
|
||||
|
||||
for (const auto& BoundingBox : Data->BoundingBoxes)
|
||||
{
|
||||
RenderInterlinesBoundingBox(PDI, BoundingBox, LineColor, RoomUnit);
|
||||
}
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Door::Tick(FEditorViewportClient* ViewportClient, float DeltaTime)
|
||||
{
|
||||
FProceduralDungeonEditorTool::Tick(ViewportClient, DeltaTime);
|
||||
|
||||
if (ShowDoorPreview)
|
||||
{
|
||||
const URoomData* Data = GetRoomData();
|
||||
check(IsValid(Data));
|
||||
|
||||
UWorld* World = ViewportClient->GetWorld();
|
||||
FDoorDef::DrawDebug(World, DoorPreview, Data->GetRoomUnit(), FTransform::Identity, /*includeOffset = */ true, /*isConnected = */ IsDoorValid(Data, DoorPreview));
|
||||
}
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEditorTool_Door::HandleClick(FEditorViewportClient* InViewportClient, HHitProxy* HitProxy, const FViewportClick& Click)
|
||||
{
|
||||
if (Click.IsAltDown() || Click.IsControlDown() || Click.IsShiftDown())
|
||||
return false;
|
||||
|
||||
if (!ShowDoorPreview)
|
||||
return false;
|
||||
|
||||
URoomData* Data = GetRoomData();
|
||||
check(IsValid(Data));
|
||||
|
||||
if (Click.GetKey() == EKeys::LeftMouseButton)
|
||||
{
|
||||
if (IsDoorValid(Data, DoorPreview))
|
||||
{
|
||||
GEditor->BeginTransaction(FText::FromString(TEXT("Add Door")));
|
||||
Data->Modify();
|
||||
Data->Doors.Add(DoorPreview);
|
||||
GEditor->EndTransaction();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (Click.GetKey() == EKeys::RightMouseButton)
|
||||
{
|
||||
if (Data->Doors.Contains(DoorPreview))
|
||||
{
|
||||
GEditor->BeginTransaction(FText::FromString(TEXT("Remove Door")));
|
||||
Data->Modify();
|
||||
Data->Doors.Remove(DoorPreview);
|
||||
GEditor->EndTransaction();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEditorTool_Door::MouseMove(FEditorViewportClient* ViewportClient, FViewport* Viewport, int32 MouseX, int32 MouseY)
|
||||
{
|
||||
ShowDoorPreview = false;
|
||||
|
||||
const URoomData* Data = GetRoomData();
|
||||
if (!IsValid(Data))
|
||||
return false;
|
||||
|
||||
FHitResult Hit;
|
||||
if (!RoomTraceFromMouse(Hit, ViewportClient))
|
||||
return false;
|
||||
|
||||
FIntVector RoomCell;
|
||||
EDoorDirection DoorDirection;
|
||||
if (!GetRoomCellFromHit(Hit, Data->GetRoomUnit(), RoomCell, DoorDirection))
|
||||
return false;
|
||||
|
||||
ShowDoorPreview = true;
|
||||
DoorPreview.Position = RoomCell;
|
||||
DoorPreview.Direction = DoorDirection;
|
||||
DoorPreview.Type = EdMode->Settings->DoorType;
|
||||
|
||||
// Snap preview to existing door if RoomCell is inside
|
||||
for (const FDoorDef& RoomDoor : Data->Doors)
|
||||
{
|
||||
if (RoomDoor.Direction != DoorPreview.Direction)
|
||||
continue;
|
||||
|
||||
if (IsPositionInside(RoomDoor, DoorPreview.Position, Data->GetRoomUnit()))
|
||||
{
|
||||
DoorPreview = RoomDoor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEditorTool_Door::GetCursor(EMouseCursor::Type& OutCursor) const
|
||||
{
|
||||
if (!ShowDoorPreview)
|
||||
return false;
|
||||
|
||||
const URoomData* Data = GetRoomData();
|
||||
if (!IsValid(Data))
|
||||
return false;
|
||||
|
||||
OutCursor = IsDoorValid(Data, DoorPreview) ? EMouseCursor::Hand : EMouseCursor::SlashedCircle;
|
||||
return true;
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Door::OnLevelChanged(const ARoomLevel* NewLevel)
|
||||
{
|
||||
UpdateCollision();
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Door::OnDataChanged(const URoomData* NewData)
|
||||
{
|
||||
UpdateCollision();
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Door::OnDataPropertiesChanged(const URoomData* Data)
|
||||
{
|
||||
UpdateCollision();
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Door::CreateCollision(ARoomLevel* Level)
|
||||
{
|
||||
if (!IsValid(Level))
|
||||
return;
|
||||
|
||||
if (!IsValid(Level->Data))
|
||||
return;
|
||||
|
||||
check(IsValid(Level->GetWorld()));
|
||||
|
||||
const int32 NumBoxes = Level->Data->BoundingBoxes.Num();
|
||||
const int32 Delta = NumBoxes - RoomBoxes.Num();
|
||||
|
||||
// Create new boxes when delta is positive
|
||||
for (int i = 0; i < Delta; ++i)
|
||||
{
|
||||
FName BoxName = FName(*FString::Printf(TEXT("Editor Room Collision %d"), RoomBoxes.Num()));
|
||||
UBoxComponent* RoomBox = NewObject<UBoxComponent>(Level, BoxName, RF_Transient);
|
||||
check(IsValid(RoomBox));
|
||||
RoomBox->SetupAttachment(Level->GetRootComponent());
|
||||
RoomBox->RegisterComponent();
|
||||
RoomBox->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
|
||||
RoomBox->SetCollisionObjectType(ECollisionChannel::ECC_MAX);
|
||||
RoomBoxes.Add(RoomBox);
|
||||
DungeonEd_LogInfo("Created RoomBox: %s", *GetNameSafe(RoomBox));
|
||||
}
|
||||
|
||||
// Destroy boxes when delta is negative
|
||||
for (int i = 0; i > Delta; --i)
|
||||
{
|
||||
if (RoomBoxes.Num() == 0)
|
||||
break;
|
||||
TWeakObjectPtr<UBoxComponent> RoomBox = RoomBoxes.Pop();
|
||||
if (!RoomBox.IsValid())
|
||||
continue;
|
||||
RoomBox->DestroyComponent();
|
||||
DungeonEd_LogInfo("Destroyed RoomBox: %s", *GetNameSafe(RoomBox.Get()));
|
||||
}
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Door::UpdateCollision()
|
||||
{
|
||||
auto Level = EdMode->GetLevelInstance();
|
||||
if (Level != CachedLevel)
|
||||
{
|
||||
DestroyCollision();
|
||||
CachedLevel = Level;
|
||||
}
|
||||
|
||||
|
||||
if (!CachedLevel.IsValid())
|
||||
return;
|
||||
|
||||
URoomData* Data = CachedLevel->Data;
|
||||
if (!IsValid(Data))
|
||||
return;
|
||||
|
||||
CreateCollision(CachedLevel.Get());
|
||||
check(Data->BoundingBoxes.Num() == RoomBoxes.Num());
|
||||
|
||||
for (int i = 0; i < Data->BoundingBoxes.Num(); ++i)
|
||||
{
|
||||
const FBoxMinAndMax& DataBox = Data->BoundingBoxes[i];
|
||||
UBoxComponent* RoomBox = RoomBoxes[i].Get();
|
||||
|
||||
FBoxCenterAndExtent Box = Dungeon::ToWorld(DataBox, Data->GetRoomUnit());
|
||||
RoomBox->SetRelativeLocation(Box.Center);
|
||||
RoomBox->SetBoxExtent(Box.Extent);
|
||||
|
||||
DungeonEd_LogInfo("Updated RoomBox: %s", *GetNameSafe(RoomBox));
|
||||
}
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Door::DestroyCollision()
|
||||
{
|
||||
for (const auto& RoomBox : RoomBoxes)
|
||||
{
|
||||
if (!RoomBox.IsValid())
|
||||
continue;
|
||||
|
||||
RoomBox->DestroyComponent();
|
||||
}
|
||||
|
||||
RoomBoxes.Empty();
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEditorTool_Door::RoomTraceFromMouse(FHitResult& OutHit, FEditorViewportClient* ViewportClient) const
|
||||
{
|
||||
int32 MouseX = ViewportClient->Viewport->GetMouseX();
|
||||
int32 MouseY = ViewportClient->Viewport->GetMouseY();
|
||||
|
||||
// Compute a world space ray from the screen space mouse coordinates
|
||||
FSceneViewFamilyContext ViewFamily(
|
||||
FSceneViewFamilyContext::ConstructionValues(ViewportClient->Viewport, ViewportClient->GetScene(), ViewportClient->EngineShowFlags)
|
||||
.SetRealtimeUpdate(ViewportClient->IsRealtime()));
|
||||
|
||||
FSceneView* View = ViewportClient->CalcSceneView(&ViewFamily);
|
||||
FViewportCursorLocation MouseViewportRay(View, ViewportClient, MouseX, MouseY);
|
||||
FVector MouseViewportRayDirection = MouseViewportRay.GetDirection();
|
||||
|
||||
FVector Start = MouseViewportRay.GetOrigin();
|
||||
FVector End = Start + WORLD_MAX * MouseViewportRayDirection;
|
||||
if (ViewportClient->IsOrtho())
|
||||
{
|
||||
Start -= WORLD_MAX * MouseViewportRayDirection;
|
||||
}
|
||||
|
||||
return RoomTrace(OutHit, Start, End);
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEditorTool_Door::RoomTrace(FHitResult& OutHit, const FVector& RayOrigin, const FVector& RayEnd) const
|
||||
{
|
||||
OutHit = FHitResult();
|
||||
FHitResult Hit;
|
||||
bool bHasHit = false;
|
||||
for (const auto& RoomBox : RoomBoxes)
|
||||
{
|
||||
if (!RoomBox.IsValid())
|
||||
continue;
|
||||
|
||||
const bool bSuccess = RoomBox->LineTraceComponent(Hit, RayOrigin, RayEnd, FCollisionQueryParams(SCENE_QUERY_STAT(RoomTrace)));
|
||||
if (!bSuccess)
|
||||
continue;
|
||||
|
||||
if (!bHasHit || Hit.Distance < OutHit.Distance)
|
||||
{
|
||||
OutHit = Hit;
|
||||
bHasHit = true;
|
||||
}
|
||||
}
|
||||
|
||||
return bHasHit;
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEditorTool_Door::GetRoomCellFromHit(const FHitResult& Hit, const FVector RoomUnit, FIntVector& OutCell, EDoorDirection& OutDirection) const
|
||||
{
|
||||
// Direction is up or down: invalid
|
||||
if (FMath::Abs(FVector::DotProduct(Hit.ImpactNormal, FVector::UpVector)) >= 0.5f)
|
||||
return false;
|
||||
|
||||
// Determine direction from the hit normal
|
||||
float DirX = FVector::DotProduct(Hit.ImpactNormal, FVector::ForwardVector);
|
||||
float DirY = FVector::DotProduct(Hit.ImpactNormal, FVector::RightVector);
|
||||
if (FMath::Abs(DirX) > FMath::Abs(DirY))
|
||||
OutDirection = (DirX > 0) ? EDoorDirection::North : EDoorDirection::South;
|
||||
else
|
||||
OutDirection = (DirY > 0) ? EDoorDirection::East : EDoorDirection::West;
|
||||
|
||||
// Determine the room cell
|
||||
FVector RoomSpacePoint = Hit.ImpactPoint / RoomUnit;
|
||||
RoomSpacePoint -= 0.5f * (ToVector(OutDirection) + FVector::UpVector);
|
||||
OutCell = FIntVector(FMath::RoundToInt(RoomSpacePoint.X), FMath::RoundToInt(RoomSpacePoint.Y), FMath::RoundToInt(RoomSpacePoint.Z));
|
||||
return true;
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ProceduralDungeonEditorTool.h"
|
||||
|
||||
class ARoomLevel;
|
||||
|
||||
class FProceduralDungeonEditorTool_Door : public FProceduralDungeonEditorTool
|
||||
{
|
||||
public:
|
||||
FProceduralDungeonEditorTool_Door(FProceduralDungeonEdMode* InEdMode)
|
||||
: FProceduralDungeonEditorTool(InEdMode)
|
||||
{
|
||||
}
|
||||
|
||||
//~ Begin FProceduralDungeonEditorTool Interface
|
||||
virtual const TCHAR* GetToolName() override { return TEXT("Tool_Door"); }
|
||||
virtual FText GetDisplayName() override { return NSLOCTEXT("ProceduralDungeonEditor", "Tool_Door", "Door"); }
|
||||
virtual FText GetDisplayMessage() override { return NSLOCTEXT("ProceduralDungeonEditor", "Tool_Door_Tooltip", ""); }
|
||||
|
||||
virtual void EnterTool() override;
|
||||
virtual void ExitTool() override;
|
||||
virtual void Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI) override;
|
||||
virtual void Tick(FEditorViewportClient* ViewportClient, float DeltaTime) override;
|
||||
virtual bool HandleClick(FEditorViewportClient* InViewportClient, HHitProxy* HitProxy, const FViewportClick& Click) override;
|
||||
virtual bool MouseMove(FEditorViewportClient* ViewportClient, FViewport* Viewport, int32 MouseX, int32 MouseY) override;
|
||||
virtual bool GetCursor(EMouseCursor::Type& OutCursor) const override;
|
||||
|
||||
virtual void OnLevelChanged(const ARoomLevel* NewLevel) override;
|
||||
virtual void OnDataChanged(const URoomData* NewData) override;
|
||||
virtual void OnDataPropertiesChanged(const URoomData* Data) override;
|
||||
//~ End FProceduralDungeonEditorTool Interface
|
||||
|
||||
protected:
|
||||
void CreateCollision(ARoomLevel* level);
|
||||
void UpdateCollision();
|
||||
void DestroyCollision();
|
||||
bool RoomTraceFromMouse(FHitResult& OutHit, FEditorViewportClient* ViewportClient) const;
|
||||
bool RoomTrace(FHitResult& OutHit, const FVector& RayOrigin, const FVector& RayEnd) const;
|
||||
bool GetRoomCellFromHit(const FHitResult& Hit, const FVector RoomUnit, FIntVector& OutCell, EDoorDirection& OutDirection) const;
|
||||
|
||||
private:
|
||||
TWeakObjectPtr<ARoomLevel> CachedLevel = nullptr;
|
||||
TArray<TWeakObjectPtr<class UBoxComponent>> RoomBoxes;
|
||||
FDoorDef DoorPreview;
|
||||
bool ShowDoorPreview {false};
|
||||
};
|
||||
+337
@@ -0,0 +1,337 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#include "ProceduralDungeonEditorTool_Size.h"
|
||||
#include "EditorMode/ProceduralDungeonEdMode.h"
|
||||
#include "EditorModeManager.h"
|
||||
#include "Engine/Selection.h"
|
||||
#include "ProceduralDungeonEdLog.h"
|
||||
#include "ProceduralDungeonUtils.h"
|
||||
#include "Room.h"
|
||||
#include "RoomLevel.h"
|
||||
#include "RoomData.h"
|
||||
|
||||
IMPLEMENT_HIT_PROXY(HRoomPointProxy, HHitProxy);
|
||||
|
||||
void FRoomPoint::AddLinkedPoint(FRoomPoint& Point, EAxisList::Type Axis)
|
||||
{
|
||||
LinkedPoints.Add({&Point, Axis});
|
||||
Point.LinkedPoints.Add({this, Axis});
|
||||
}
|
||||
|
||||
void FRoomPoint::SetLocation(FVector InLocation)
|
||||
{
|
||||
SetDirty();
|
||||
Location = InLocation;
|
||||
UpdateWithPropagation();
|
||||
}
|
||||
|
||||
void FRoomPoint::SetDirty()
|
||||
{
|
||||
if (bDirty)
|
||||
return;
|
||||
|
||||
bDirty = EAxisList::XYZ;
|
||||
for (FLink& Link : LinkedPoints)
|
||||
{
|
||||
check(Link.Point);
|
||||
Link.Point->SetDirty();
|
||||
}
|
||||
}
|
||||
|
||||
void FRoomPoint::UpdateWithPropagation()
|
||||
{
|
||||
TQueue<FRoomPoint*> PendingNodes;
|
||||
PendingNodes.Enqueue(this);
|
||||
FRoomPoint* Node = nullptr;
|
||||
while (PendingNodes.Dequeue(Node))
|
||||
{
|
||||
Node->UpdateLinkedPoints(PendingNodes);
|
||||
}
|
||||
}
|
||||
|
||||
void FRoomPoint::UpdateLinkedPoints(TQueue<FRoomPoint*>& PendingNodes)
|
||||
{
|
||||
for (FLink& Link : LinkedPoints)
|
||||
{
|
||||
check(Link.Point);
|
||||
if (Link.Point->bDirty)
|
||||
{
|
||||
Link.Point->UpdateFrom(*this, Link.Axis);
|
||||
PendingNodes.Enqueue(Link.Point);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FRoomPoint::UpdateFrom(FRoomPoint& From, EAxisList::Type Axis)
|
||||
{
|
||||
if (bDirty & Axis & EAxisList::X)
|
||||
{
|
||||
bDirty &= ~EAxisList::X;
|
||||
From.bDirty &= ~EAxisList::X;
|
||||
Location.X = From.Location.X;
|
||||
}
|
||||
if (bDirty & Axis & EAxisList::Y)
|
||||
{
|
||||
bDirty &= ~EAxisList::Y;
|
||||
From.bDirty &= ~EAxisList::Y;
|
||||
Location.Y = From.Location.Y;
|
||||
}
|
||||
if (bDirty & Axis & EAxisList::Z)
|
||||
{
|
||||
bDirty &= ~EAxisList::Z;
|
||||
From.bDirty &= ~EAxisList::Z;
|
||||
Location.Z = From.Location.Z;
|
||||
}
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Size::EnterTool()
|
||||
{
|
||||
OnDataChanged();
|
||||
ResetSelectedPoint();
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Size::ExitTool()
|
||||
{
|
||||
DungeonEd_LogInfo("Exit Size Tool.");
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Size::Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI)
|
||||
{
|
||||
const URoomData* Data = GetRoomData();
|
||||
if (!IsValid(Data))
|
||||
return;
|
||||
|
||||
for (const FBoxPoints& Box : Boxes)
|
||||
{
|
||||
Box.Draw(PDI, SelectedPoint);
|
||||
}
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEditorTool_Size::HandleClick(FEditorViewportClient* InViewportClient, HHitProxy* HitProxy, const FViewportClick& Click)
|
||||
{
|
||||
ResetSelectedPoint();
|
||||
|
||||
if (!HitProxy)
|
||||
return false;
|
||||
|
||||
if (!HitProxy->IsA(HRoomPointProxy::StaticGetType()))
|
||||
return false;
|
||||
|
||||
const HRoomPointProxy* RoomProxy = (HRoomPointProxy*)HitProxy;
|
||||
checkf(RoomProxy->Index >= 0 && RoomProxy->Index < GetMaxIndex(), TEXT("A room point has been clicked but is out of bounds!"));
|
||||
|
||||
SetSelectedPoint(RoomProxy->Index);
|
||||
GEditor->GetSelectedActors()->DeselectAll();
|
||||
|
||||
// Force translate widget when a point is selected.
|
||||
FEditorModeTools* ModeTools = EdMode->GetModeManager();
|
||||
if (ModeTools)
|
||||
ModeTools->SetWidgetMode(WidgetMode::WM_Translate);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEditorTool_Size::InputKey(FEditorViewportClient* InViewportClient, FViewport* InViewport, FKey InKey, EInputEvent InEvent)
|
||||
{
|
||||
if (InKey == EKeys::LeftMouseButton && InEvent == IE_Released)
|
||||
ResetDragPoint();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEditorTool_Size::InputDelta(FEditorViewportClient* InViewportClient, FViewport* InViewport, FVector& InDrag, FRotator& InRot, FVector& InScale)
|
||||
{
|
||||
if (InViewportClient->GetCurrentWidgetAxis() == EAxisList::None)
|
||||
return false;
|
||||
|
||||
if (!HasValidSelection())
|
||||
return false;
|
||||
|
||||
const URoomData* Data = GetRoomData();
|
||||
if (!IsValid(Data))
|
||||
return false;
|
||||
|
||||
if (!InDrag.IsNearlyZero())
|
||||
{
|
||||
DragPoint += InDrag;
|
||||
FRoomPoint& Point = GetSelectedPoint();
|
||||
FVector OldPoint = Point.GetLocation();
|
||||
Point.SetLocation(Dungeon::SnapPoint(DragPoint, Data->GetRoomUnit()));
|
||||
if (OldPoint != Point.GetLocation())
|
||||
UpdateDataAsset(GetBoxIndexFromPointIndex(SelectedPoint));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEditorTool_Size::UsesTransformWidget() const
|
||||
{
|
||||
return HasValidSelection() || (GEditor->GetSelectedActorCount() > 0);
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEditorTool_Size::UsesTransformWidget(WidgetMode CheckMode) const
|
||||
{
|
||||
return HasValidSelection() ? (CheckMode == WidgetMode::WM_Translate) : EdMode->FEdMode::UsesTransformWidget(CheckMode);
|
||||
}
|
||||
|
||||
FVector FProceduralDungeonEditorTool_Size::GetWidgetLocation() const
|
||||
{
|
||||
return HasValidSelection() ? DragPoint : EdMode->FEdMode::GetWidgetLocation();
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Size::PostUndo(bool bSuccess)
|
||||
{
|
||||
OnDataChanged();
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Size::PostRedo(bool bSuccess)
|
||||
{
|
||||
OnDataChanged();
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Size::OnDataChanged(const URoomData* NewData)
|
||||
{
|
||||
// When the room data has changed, reset all
|
||||
ResetSelectedPoint();
|
||||
ResetDragPoint();
|
||||
|
||||
UpdateBoxes(NewData);
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Size::OnDataPropertiesChanged(const URoomData* Data)
|
||||
{
|
||||
UpdateBoxes(Data);
|
||||
}
|
||||
|
||||
bool FProceduralDungeonEditorTool_Size::HasValidSelection() const
|
||||
{
|
||||
return SelectedPoint >= 0 && SelectedPoint < GetMaxIndex();
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Size::ResetDragPoint()
|
||||
{
|
||||
if (HasValidSelection())
|
||||
DragPoint = GetSelectedPoint().GetLocation();
|
||||
else
|
||||
DragPoint = FVector::ZeroVector;
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Size::UpdateDataAsset(int32 BoxIndex) const
|
||||
{
|
||||
URoomData* Data = GetRoomData();
|
||||
if (!IsValid(Data))
|
||||
return;
|
||||
|
||||
if (BoxIndex >= 0 && BoxIndex < Data->BoundingBoxes.Num())
|
||||
{
|
||||
Data->Modify();
|
||||
const FVector RoomUnit = Data->GetRoomUnit();
|
||||
const FBoxPoints& Box = Boxes[BoxIndex];
|
||||
FBoxMinAndMax& DataBox = Data->BoundingBoxes[BoxIndex];
|
||||
const FIntVector A = Dungeon::ToRoomLocation(Box.GetMin(), RoomUnit);
|
||||
const FIntVector B = Dungeon::ToRoomLocation(Box.GetMax(), RoomUnit);
|
||||
DataBox.SetMinAndMax(A, B);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Size::SetSelectedPoint(int32 Index)
|
||||
{
|
||||
if (Index < 0 || Index >= GetMaxIndex())
|
||||
Index = -1;
|
||||
|
||||
DungeonEd_LogInfo("Selected Point: %d", Index);
|
||||
SelectedPoint = Index;
|
||||
ResetDragPoint();
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Size::ResetSelectedPoint()
|
||||
{
|
||||
SetSelectedPoint(-1);
|
||||
}
|
||||
|
||||
FRoomPoint& FProceduralDungeonEditorTool_Size::GetSelectedPoint()
|
||||
{
|
||||
checkf(HasValidSelection(), TEXT("Trying to get selected point but selection is invalid!"));
|
||||
const int32 BoxIndex = GetBoxIndexFromPointIndex(SelectedPoint);
|
||||
const int32 PointIndex = GetBoxPointIndex(SelectedPoint);
|
||||
checkf(BoxIndex >= 0 && BoxIndex < Boxes.Num(), TEXT("Selected point's box index is out of bounds!"));
|
||||
checkf(PointIndex >= 0 && PointIndex < FBoxPoints::NbPoints, TEXT("Selected point's point index is out of bounds!"));
|
||||
return Boxes[BoxIndex].GetPoint(PointIndex);
|
||||
}
|
||||
|
||||
void FProceduralDungeonEditorTool_Size::UpdateBoxes(const URoomData* Data)
|
||||
{
|
||||
if (!IsValid(Data))
|
||||
return;
|
||||
|
||||
const int32 NbBoxes = Data->BoundingBoxes.Num();
|
||||
if (Boxes.Num() != NbBoxes)
|
||||
{
|
||||
Boxes.SetNum(NbBoxes);
|
||||
}
|
||||
|
||||
const FVector RoomUnit = Data->GetRoomUnit();
|
||||
for (int32 i = 0; i < NbBoxes; ++i)
|
||||
{
|
||||
const FBoxMinAndMax& Box = Data->BoundingBoxes[i];
|
||||
Boxes[i].SetID(i);
|
||||
Boxes[i].SetMin(Dungeon::ToWorldLocation(Box.GetMin(), RoomUnit));
|
||||
Boxes[i].SetMax(Dungeon::ToWorldLocation(Box.GetMax(), RoomUnit));
|
||||
}
|
||||
}
|
||||
|
||||
FBoxPoints::FBoxPoints()
|
||||
{
|
||||
// 6 1
|
||||
// 2 7
|
||||
// 4 5
|
||||
// 0 3
|
||||
|
||||
Points[0].AddLinkedPoint(Points[2], EAxisList::XY);
|
||||
Points[0].AddLinkedPoint(Points[3], EAxisList::XZ);
|
||||
Points[0].AddLinkedPoint(Points[4], EAxisList::YZ);
|
||||
|
||||
Points[1].AddLinkedPoint(Points[5], EAxisList::XY);
|
||||
Points[1].AddLinkedPoint(Points[6], EAxisList::XZ);
|
||||
Points[1].AddLinkedPoint(Points[7], EAxisList::YZ);
|
||||
|
||||
Points[2].AddLinkedPoint(Points[6], EAxisList::YZ);
|
||||
Points[2].AddLinkedPoint(Points[7], EAxisList::XZ);
|
||||
|
||||
Points[3].AddLinkedPoint(Points[5], EAxisList::YZ);
|
||||
Points[3].AddLinkedPoint(Points[7], EAxisList::XY);
|
||||
|
||||
Points[4].AddLinkedPoint(Points[5], EAxisList::XZ);
|
||||
Points[4].AddLinkedPoint(Points[6], EAxisList::XY);
|
||||
}
|
||||
|
||||
void FBoxPoints::SetMin(FVector Value)
|
||||
{
|
||||
Points[0].SetLocation(Value);
|
||||
}
|
||||
|
||||
void FBoxPoints::SetMax(FVector Value)
|
||||
{
|
||||
Points[1].SetLocation(Value);
|
||||
}
|
||||
|
||||
void FBoxPoints::Draw(FPrimitiveDrawInterface* PDI, int32 SelectedPoint) const
|
||||
{
|
||||
static const FColor NormalColor(200, 200, 200);
|
||||
static const FColor SelectedColor(255, 128, 0);
|
||||
|
||||
const int32 LocalSelectedIndex = SelectedPoint - (ID * NbPoints);
|
||||
for (int i = 0; i < NbPoints; ++i)
|
||||
{
|
||||
const bool bSelected = (LocalSelectedIndex == i);
|
||||
const FColor& Color = bSelected ? SelectedColor : NormalColor;
|
||||
|
||||
PDI->SetHitProxy(new HRoomPointProxy(ID * NbPoints + i));
|
||||
PDI->DrawPoint(Points[i].GetLocation(), Color, 10.0f, SDPG_Foreground);
|
||||
PDI->SetHitProxy(nullptr);
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
|
||||
//
|
||||
// This software is available under different licenses depending on the source from which it was obtained:
|
||||
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
|
||||
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
|
||||
// Please refer to the accompanying LICENSE file for further details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "ProceduralDungeonEditorTool.h"
|
||||
|
||||
struct HRoomPointProxy : public HHitProxy
|
||||
{
|
||||
DECLARE_HIT_PROXY();
|
||||
|
||||
HRoomPointProxy(int32 InIndex)
|
||||
: HHitProxy(HPP_UI)
|
||||
, Index(InIndex)
|
||||
{
|
||||
}
|
||||
|
||||
int32 Index {-1};
|
||||
};
|
||||
|
||||
struct FRoomPoint
|
||||
{
|
||||
struct FLink
|
||||
{
|
||||
FRoomPoint* Point;
|
||||
EAxisList::Type Axis;
|
||||
};
|
||||
|
||||
void AddLinkedPoint(FRoomPoint& Point, EAxisList::Type Axis);
|
||||
FVector GetLocation() const { return Location; }
|
||||
void SetLocation(FVector InLocation);
|
||||
FVector* operator->() { return &Location; }
|
||||
|
||||
private:
|
||||
void SetDirty();
|
||||
void UpdateWithPropagation();
|
||||
void UpdateLinkedPoints(TQueue<FRoomPoint*>& PendingNodes);
|
||||
void UpdateFrom(FRoomPoint& From, EAxisList::Type Axis);
|
||||
|
||||
private:
|
||||
EAxisList::Type bDirty {EAxisList::None};
|
||||
FVector Location {0};
|
||||
TArray<FLink> LinkedPoints {};
|
||||
};
|
||||
|
||||
struct FBoxPoints
|
||||
{
|
||||
public:
|
||||
FBoxPoints();
|
||||
|
||||
void SetID(uint32 InID) { ID = InID; }
|
||||
void SetMin(FVector Value);
|
||||
void SetMax(FVector Value);
|
||||
uint32 GetID() const { return ID; }
|
||||
FVector GetMin() const { return Points[0].GetLocation(); }
|
||||
FVector GetMax() const { return Points[1].GetLocation(); }
|
||||
|
||||
FRoomPoint& GetPoint(int32 Index)
|
||||
{
|
||||
check(Index >= 0 && Index < NbPoints);
|
||||
return Points[Index];
|
||||
}
|
||||
|
||||
void Draw(FPrimitiveDrawInterface* PDI, int32 SelectedPoint) const;
|
||||
|
||||
static const uint32 NbPoints {8};
|
||||
|
||||
private:
|
||||
uint32 ID;
|
||||
FRoomPoint Points[NbPoints];
|
||||
};
|
||||
|
||||
class FProceduralDungeonEditorTool_Size : public FProceduralDungeonEditorTool
|
||||
{
|
||||
public:
|
||||
FProceduralDungeonEditorTool_Size(FProceduralDungeonEdMode* InEdMode)
|
||||
: FProceduralDungeonEditorTool(InEdMode)
|
||||
{
|
||||
}
|
||||
|
||||
virtual const TCHAR* GetToolName() override { return TEXT("Tool_Size"); }
|
||||
virtual FText GetDisplayName() override { return NSLOCTEXT("ProceduralDungeonEditor", "Tool_Size", "Size"); }
|
||||
virtual FText GetDisplayMessage() override { return NSLOCTEXT("ProceduralDungeonEditor", "Tool_Size_Tooltip", ""); }
|
||||
|
||||
virtual void EnterTool() override;
|
||||
virtual void ExitTool() override;
|
||||
|
||||
virtual void Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI) override;
|
||||
virtual bool HandleClick(FEditorViewportClient* InViewportClient, HHitProxy* HitProxy, const FViewportClick& Click) override;
|
||||
virtual bool InputKey(FEditorViewportClient* InViewportClient, FViewport* InViewport, FKey InKey, EInputEvent InEvent) override;
|
||||
virtual bool InputDelta(FEditorViewportClient* InViewportClient, FViewport* InViewport, FVector& InDrag, FRotator& InRot, FVector& InScale) override;
|
||||
virtual bool UsesTransformWidget() const override;
|
||||
virtual bool UsesTransformWidget(WidgetMode CheckMode) const override;
|
||||
virtual FVector GetWidgetLocation() const override;
|
||||
|
||||
/** FEditorUndoClient interface */
|
||||
virtual void PostUndo(bool bSuccess) override;
|
||||
virtual void PostRedo(bool bSuccess) override;
|
||||
|
||||
virtual void OnDataChanged(const URoomData* NewData = nullptr) override;
|
||||
virtual void OnDataPropertiesChanged(const URoomData* Data = nullptr) override;
|
||||
|
||||
bool HasValidSelection() const;
|
||||
void ResetDragPoint();
|
||||
void UpdateDataAsset(int32 BoxIndex) const;
|
||||
|
||||
void SetSelectedPoint(int32 Index);
|
||||
void ResetSelectedPoint();
|
||||
|
||||
private:
|
||||
int32 GetBoxIndexFromPointIndex(int32 PointIndex) const { return PointIndex / FBoxPoints::NbPoints; }
|
||||
int32 GetBoxPointIndex(int32 PointIndex) const { return PointIndex % FBoxPoints::NbPoints; }
|
||||
int32 GetMaxIndex() const { return Boxes.Num() * FBoxPoints::NbPoints; }
|
||||
FRoomPoint& GetSelectedPoint();
|
||||
void UpdateBoxes(const URoomData* Data);
|
||||
|
||||
private:
|
||||
int32 SelectedPoint {-1};
|
||||
TArray<FBoxPoints> Boxes;
|
||||
FVector DragPoint;
|
||||
};
|
||||
Reference in New Issue
Block a user