Add duplicate (Ctrl+D) and copy/paste (Ctrl+C/V) with undo support
Duplicate clones selected actors with a small offset and selects the copies. Copy stores actor class and relative transforms in an internal clipboard. Paste spawns clipboard contents in front of the camera preserving relative positions. All operations are tracked by SpawnManager and fully undoable/redoable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
@@ -186,6 +186,19 @@ void APS_Editor_Pawn::CreateInputActions()
|
|||||||
IA_Redo = NewObject<UInputAction>(this, TEXT("IA_Redo"));
|
IA_Redo = NewObject<UInputAction>(this, TEXT("IA_Redo"));
|
||||||
IA_Redo->ValueType = EInputActionValueType::Boolean;
|
IA_Redo->ValueType = EInputActionValueType::Boolean;
|
||||||
EditorMappingContext->MapKey(IA_Redo, EKeys::Y);
|
EditorMappingContext->MapKey(IA_Redo, EKeys::Y);
|
||||||
|
|
||||||
|
// Duplicate / Copy / Paste (Ctrl+D / Ctrl+C / Ctrl+V)
|
||||||
|
IA_Duplicate = NewObject<UInputAction>(this, TEXT("IA_Duplicate"));
|
||||||
|
IA_Duplicate->ValueType = EInputActionValueType::Boolean;
|
||||||
|
EditorMappingContext->MapKey(IA_Duplicate, EKeys::D);
|
||||||
|
|
||||||
|
IA_Copy = NewObject<UInputAction>(this, TEXT("IA_Copy"));
|
||||||
|
IA_Copy->ValueType = EInputActionValueType::Boolean;
|
||||||
|
EditorMappingContext->MapKey(IA_Copy, EKeys::C);
|
||||||
|
|
||||||
|
IA_Paste = NewObject<UInputAction>(this, TEXT("IA_Paste"));
|
||||||
|
IA_Paste->ValueType = EInputActionValueType::Boolean;
|
||||||
|
EditorMappingContext->MapKey(IA_Paste, EKeys::V);
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
||||||
@@ -226,6 +239,9 @@ void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComp
|
|||||||
EIC->BindAction(IA_SnapToGround, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleSnapToGround);
|
EIC->BindAction(IA_SnapToGround, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleSnapToGround);
|
||||||
EIC->BindAction(IA_Undo, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleUndo);
|
EIC->BindAction(IA_Undo, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleUndo);
|
||||||
EIC->BindAction(IA_Redo, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleRedo);
|
EIC->BindAction(IA_Redo, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleRedo);
|
||||||
|
EIC->BindAction(IA_Duplicate, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleDuplicate);
|
||||||
|
EIC->BindAction(IA_Copy, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleCopy);
|
||||||
|
EIC->BindAction(IA_Paste, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandlePaste);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Input Handlers ----
|
// ---- Input Handlers ----
|
||||||
@@ -708,6 +724,42 @@ void APS_Editor_Pawn::HandleSnapToGround(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Pawn::HandleDuplicate(const FInputActionValue& Value)
|
||||||
|
{
|
||||||
|
if (!bCtrlHeld || CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||||
|
{
|
||||||
|
SM->DuplicateSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Pawn::HandleCopy(const FInputActionValue& Value)
|
||||||
|
{
|
||||||
|
if (!bCtrlHeld || CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||||
|
{
|
||||||
|
SM->CopySelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Pawn::HandlePaste(const FInputActionValue& Value)
|
||||||
|
{
|
||||||
|
if (!bCtrlHeld || CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||||
|
{
|
||||||
|
SM->PasteClipboard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
|
|
||||||
void APS_Editor_Pawn::SetCameraMode(EPS_Editor_CameraMode NewMode)
|
void APS_Editor_Pawn::SetCameraMode(EPS_Editor_CameraMode NewMode)
|
||||||
|
|||||||
@@ -171,6 +171,15 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<UInputAction> IA_Redo;
|
TObjectPtr<UInputAction> IA_Redo;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UInputAction> IA_Duplicate;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UInputAction> IA_Copy;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UInputAction> IA_Paste;
|
||||||
|
|
||||||
// ---- Modifier key state ----
|
// ---- Modifier key state ----
|
||||||
bool bAltHeld = false;
|
bool bAltHeld = false;
|
||||||
bool bCtrlHeld = false;
|
bool bCtrlHeld = false;
|
||||||
@@ -204,6 +213,9 @@ private:
|
|||||||
void HandleSnapToGround(const FInputActionValue& Value);
|
void HandleSnapToGround(const FInputActionValue& Value);
|
||||||
void HandleUndo(const FInputActionValue& Value);
|
void HandleUndo(const FInputActionValue& Value);
|
||||||
void HandleRedo(const FInputActionValue& Value);
|
void HandleRedo(const FInputActionValue& Value);
|
||||||
|
void HandleDuplicate(const FInputActionValue& Value);
|
||||||
|
void HandleCopy(const FInputActionValue& Value);
|
||||||
|
void HandlePaste(const FInputActionValue& Value);
|
||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
void CreateInputActions();
|
void CreateInputActions();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "PS_Editor_Gizmo.h"
|
#include "PS_Editor_Gizmo.h"
|
||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
|
#include "PS_Editor_SpawnManager.h"
|
||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "Engine/World.h"
|
#include "Engine/World.h"
|
||||||
#include "GameFramework/Pawn.h"
|
#include "GameFramework/Pawn.h"
|
||||||
@@ -377,6 +378,168 @@ void UPS_Editor_SelectionManager::UpdateGizmo()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_SelectionManager::DuplicateSelection()
|
||||||
|
{
|
||||||
|
APlayerController* PC = OwnerPC.Get();
|
||||||
|
if (!PC || !IsAnythingSelected()) return;
|
||||||
|
|
||||||
|
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC);
|
||||||
|
if (!EditorPC) return;
|
||||||
|
|
||||||
|
static const FVector DuplicateOffset(50.0f, 50.0f, 0.0f);
|
||||||
|
TArray<AActor*> NewActors;
|
||||||
|
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SelectedActors)
|
||||||
|
{
|
||||||
|
AActor* Original = Weak.Get();
|
||||||
|
if (!Original) continue;
|
||||||
|
|
||||||
|
FActorSpawnParameters SpawnParams;
|
||||||
|
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
|
||||||
|
|
||||||
|
AActor* NewActor = PC->GetWorld()->SpawnActor<AActor>(
|
||||||
|
Original->GetClass(),
|
||||||
|
Original->GetActorLocation() + DuplicateOffset,
|
||||||
|
Original->GetActorRotation(),
|
||||||
|
SpawnParams);
|
||||||
|
|
||||||
|
if (NewActor)
|
||||||
|
{
|
||||||
|
NewActor->SetActorScale3D(Original->GetActorScale3D());
|
||||||
|
if (USceneComponent* Root = NewActor->GetRootComponent())
|
||||||
|
{
|
||||||
|
Root->SetMobility(EComponentMobility::Movable);
|
||||||
|
}
|
||||||
|
NewActors.Add(NewActor);
|
||||||
|
|
||||||
|
if (UPS_Editor_SpawnManager* SpawnMgr = EditorPC->GetSpawnManager())
|
||||||
|
{
|
||||||
|
SpawnMgr->TrackSpawnedActor(NewActor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NewActors.Num() == 0) return;
|
||||||
|
|
||||||
|
// Record undo action
|
||||||
|
TSharedPtr<FPS_Editor_SpawnAction> Action = MakeShared<FPS_Editor_SpawnAction>();
|
||||||
|
for (AActor* Actor : NewActors)
|
||||||
|
{
|
||||||
|
Action->Actors.Add(Actor);
|
||||||
|
}
|
||||||
|
Action->Description = FString::Printf(TEXT("Duplicate %d actor(s)"), NewActors.Num());
|
||||||
|
|
||||||
|
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||||
|
{
|
||||||
|
UM->RecordAction(Action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select the duplicates (deselect originals)
|
||||||
|
ClearSelection();
|
||||||
|
for (AActor* Actor : NewActors)
|
||||||
|
{
|
||||||
|
SelectActor(Actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Duplicated %d actor(s)"), NewActors.Num());
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_SelectionManager::CopySelection()
|
||||||
|
{
|
||||||
|
Clipboard.Empty();
|
||||||
|
|
||||||
|
if (!IsAnythingSelected()) return;
|
||||||
|
|
||||||
|
const FVector Pivot = GetSelectionPivot();
|
||||||
|
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SelectedActors)
|
||||||
|
{
|
||||||
|
AActor* Actor = Weak.Get();
|
||||||
|
if (!Actor) continue;
|
||||||
|
|
||||||
|
FPS_Editor_ClipboardEntry Entry;
|
||||||
|
Entry.ActorClass = Actor->GetClass();
|
||||||
|
Entry.RelativeLocation = Actor->GetActorLocation() - Pivot;
|
||||||
|
Entry.Rotation = Actor->GetActorRotation();
|
||||||
|
Entry.Scale = Actor->GetActorScale3D();
|
||||||
|
Clipboard.Add(Entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Copied %d actor(s) to clipboard"), Clipboard.Num());
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_SelectionManager::PasteClipboard()
|
||||||
|
{
|
||||||
|
if (Clipboard.Num() == 0) return;
|
||||||
|
|
||||||
|
APlayerController* PC = OwnerPC.Get();
|
||||||
|
if (!PC) return;
|
||||||
|
|
||||||
|
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC);
|
||||||
|
if (!EditorPC) return;
|
||||||
|
|
||||||
|
// Paste center: in front of the camera
|
||||||
|
FVector CamLoc;
|
||||||
|
FRotator CamRot;
|
||||||
|
PC->GetPlayerViewPoint(CamLoc, CamRot);
|
||||||
|
const FVector PasteCenter = CamLoc + CamRot.Vector() * 500.0f;
|
||||||
|
|
||||||
|
TArray<AActor*> NewActors;
|
||||||
|
|
||||||
|
for (const FPS_Editor_ClipboardEntry& Entry : Clipboard)
|
||||||
|
{
|
||||||
|
if (!Entry.ActorClass) continue;
|
||||||
|
|
||||||
|
FActorSpawnParameters SpawnParams;
|
||||||
|
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
|
||||||
|
|
||||||
|
AActor* NewActor = PC->GetWorld()->SpawnActor<AActor>(
|
||||||
|
Entry.ActorClass,
|
||||||
|
PasteCenter + Entry.RelativeLocation,
|
||||||
|
Entry.Rotation,
|
||||||
|
SpawnParams);
|
||||||
|
|
||||||
|
if (NewActor)
|
||||||
|
{
|
||||||
|
NewActor->SetActorScale3D(Entry.Scale);
|
||||||
|
if (USceneComponent* Root = NewActor->GetRootComponent())
|
||||||
|
{
|
||||||
|
Root->SetMobility(EComponentMobility::Movable);
|
||||||
|
}
|
||||||
|
NewActors.Add(NewActor);
|
||||||
|
|
||||||
|
if (UPS_Editor_SpawnManager* SpawnMgr = EditorPC->GetSpawnManager())
|
||||||
|
{
|
||||||
|
SpawnMgr->TrackSpawnedActor(NewActor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NewActors.Num() == 0) return;
|
||||||
|
|
||||||
|
// Record undo action
|
||||||
|
TSharedPtr<FPS_Editor_SpawnAction> Action = MakeShared<FPS_Editor_SpawnAction>();
|
||||||
|
for (AActor* Actor : NewActors)
|
||||||
|
{
|
||||||
|
Action->Actors.Add(Actor);
|
||||||
|
}
|
||||||
|
Action->Description = FString::Printf(TEXT("Paste %d actor(s)"), NewActors.Num());
|
||||||
|
|
||||||
|
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||||
|
{
|
||||||
|
UM->RecordAction(Action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select the pasted actors
|
||||||
|
ClearSelection();
|
||||||
|
for (AActor* Actor : NewActors)
|
||||||
|
{
|
||||||
|
SelectActor(Actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Pasted %d actor(s) from clipboard"), NewActors.Num());
|
||||||
|
}
|
||||||
|
|
||||||
void UPS_Editor_SelectionManager::CleanupStaleReferences()
|
void UPS_Editor_SelectionManager::CleanupStaleReferences()
|
||||||
{
|
{
|
||||||
SelectedActors.RemoveAll([](const TWeakObjectPtr<AActor>& Weak)
|
SelectedActors.RemoveAll([](const TWeakObjectPtr<AActor>& Weak)
|
||||||
|
|||||||
@@ -8,6 +8,17 @@
|
|||||||
class APlayerController;
|
class APlayerController;
|
||||||
class APS_Editor_Gizmo;
|
class APS_Editor_Gizmo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clipboard entry for copy/paste operations.
|
||||||
|
*/
|
||||||
|
struct FPS_Editor_ClipboardEntry
|
||||||
|
{
|
||||||
|
UClass* ActorClass = nullptr;
|
||||||
|
FVector RelativeLocation = FVector::ZeroVector; // Relative to clipboard pivot
|
||||||
|
FRotator Rotation = FRotator::ZeroRotator;
|
||||||
|
FVector Scale = FVector::OneVector;
|
||||||
|
};
|
||||||
|
|
||||||
DECLARE_MULTICAST_DELEGATE(FPS_Editor_OnSelectionChanged);
|
DECLARE_MULTICAST_DELEGATE(FPS_Editor_OnSelectionChanged);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,6 +50,18 @@ public:
|
|||||||
/** Snap all selected actors to the ground below them. */
|
/** Snap all selected actors to the ground below them. */
|
||||||
void SnapSelectionToGround();
|
void SnapSelectionToGround();
|
||||||
|
|
||||||
|
/** Duplicate selected actors with a small offset. Selects the duplicates. */
|
||||||
|
void DuplicateSelection();
|
||||||
|
|
||||||
|
/** Copy selected actors to the internal clipboard. */
|
||||||
|
void CopySelection();
|
||||||
|
|
||||||
|
/** Paste clipboard contents in front of the camera. */
|
||||||
|
void PasteClipboard();
|
||||||
|
|
||||||
|
/** Returns true if the clipboard has entries. */
|
||||||
|
bool HasClipboardData() const { return Clipboard.Num() > 0; }
|
||||||
|
|
||||||
// ---- Queries ----
|
// ---- Queries ----
|
||||||
|
|
||||||
const TArray<TWeakObjectPtr<AActor>>& GetSelectedActors() const { return SelectedActors; }
|
const TArray<TWeakObjectPtr<AActor>>& GetSelectedActors() const { return SelectedActors; }
|
||||||
@@ -95,6 +118,9 @@ private:
|
|||||||
EPS_Editor_TransformMode CurrentTransformMode = EPS_Editor_TransformMode::Translate;
|
EPS_Editor_TransformMode CurrentTransformMode = EPS_Editor_TransformMode::Translate;
|
||||||
EPS_Editor_Space CurrentSpace = EPS_Editor_Space::World;
|
EPS_Editor_Space CurrentSpace = EPS_Editor_Space::World;
|
||||||
|
|
||||||
|
/** Clipboard for copy/paste. */
|
||||||
|
TArray<FPS_Editor_ClipboardEntry> Clipboard;
|
||||||
|
|
||||||
/** Toggle custom depth stencil rendering on an actor's primitive components. */
|
/** Toggle custom depth stencil rendering on an actor's primitive components. */
|
||||||
void SetActorHighlight(AActor* Actor, bool bHighlight);
|
void SetActorHighlight(AActor* Actor, bool bHighlight);
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ struct FPS_Editor_SpawnAction : public FPS_Editor_Action
|
|||||||
FPS_Editor_SpawnAction() : FPS_Editor_Action(EPS_Editor_ActionType::Spawn) {}
|
FPS_Editor_SpawnAction() : FPS_Editor_Action(EPS_Editor_ActionType::Spawn) {}
|
||||||
|
|
||||||
TArray<TWeakObjectPtr<AActor>> Actors;
|
TArray<TWeakObjectPtr<AActor>> Actors;
|
||||||
|
FString Description; // Optional override (e.g. "Duplicate", "Paste")
|
||||||
|
|
||||||
virtual void Execute() override
|
virtual void Execute() override
|
||||||
{
|
{
|
||||||
@@ -150,6 +151,7 @@ struct FPS_Editor_SpawnAction : public FPS_Editor_Action
|
|||||||
|
|
||||||
virtual FString GetDescription() const override
|
virtual FString GetDescription() const override
|
||||||
{
|
{
|
||||||
|
if (!Description.IsEmpty()) return Description;
|
||||||
return FString::Printf(TEXT("Spawn %d actor(s)"), Actors.Num());
|
return FString::Printf(TEXT("Spawn %d actor(s)"), Actors.Num());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -203,6 +203,14 @@ void UPS_Editor_SpawnManager::DestroyAllSpawnedActors()
|
|||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: All spawned actors destroyed"));
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: All spawned actors destroyed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_SpawnManager::TrackSpawnedActor(AActor* Actor)
|
||||||
|
{
|
||||||
|
if (Actor)
|
||||||
|
{
|
||||||
|
SpawnedActors.Add(Actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TArray<FString> UPS_Editor_SpawnManager::GetCategories() const
|
TArray<FString> UPS_Editor_SpawnManager::GetCategories() const
|
||||||
{
|
{
|
||||||
TArray<FString> Categories;
|
TArray<FString> Categories;
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ public:
|
|||||||
/** Get unique categories for filtering. */
|
/** Get unique categories for filtering. */
|
||||||
TArray<FString> GetCategories() const;
|
TArray<FString> GetCategories() const;
|
||||||
|
|
||||||
|
/** Register an externally-spawned actor for tracking (used by duplicate/paste). */
|
||||||
|
void TrackSpawnedActor(AActor* Actor);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TWeakObjectPtr<APlayerController> OwnerPC;
|
TWeakObjectPtr<APlayerController> OwnerPC;
|
||||||
|
|||||||
Reference in New Issue
Block a user