diff --git a/Unreal/Content/test.umap b/Unreal/Content/test.umap index 72715db..1e71822 100644 Binary files a/Unreal/Content/test.umap and b/Unreal/Content/test.umap differ diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp index 06f88b9..07b3ab2 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp @@ -186,6 +186,19 @@ void APS_Editor_Pawn::CreateInputActions() IA_Redo = NewObject(this, TEXT("IA_Redo")); IA_Redo->ValueType = EInputActionValueType::Boolean; EditorMappingContext->MapKey(IA_Redo, EKeys::Y); + + // Duplicate / Copy / Paste (Ctrl+D / Ctrl+C / Ctrl+V) + IA_Duplicate = NewObject(this, TEXT("IA_Duplicate")); + IA_Duplicate->ValueType = EInputActionValueType::Boolean; + EditorMappingContext->MapKey(IA_Duplicate, EKeys::D); + + IA_Copy = NewObject(this, TEXT("IA_Copy")); + IA_Copy->ValueType = EInputActionValueType::Boolean; + EditorMappingContext->MapKey(IA_Copy, EKeys::C); + + IA_Paste = NewObject(this, TEXT("IA_Paste")); + IA_Paste->ValueType = EInputActionValueType::Boolean; + EditorMappingContext->MapKey(IA_Paste, EKeys::V); } 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_Undo, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleUndo); 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 ---- @@ -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(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(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(GetController())) + { + if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager()) + { + SM->PasteClipboard(); + } + } +} + // ---- Helpers ---- void APS_Editor_Pawn::SetCameraMode(EPS_Editor_CameraMode NewMode) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h index 4535fcf..b6c8f84 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h @@ -171,6 +171,15 @@ private: UPROPERTY() TObjectPtr IA_Redo; + UPROPERTY() + TObjectPtr IA_Duplicate; + + UPROPERTY() + TObjectPtr IA_Copy; + + UPROPERTY() + TObjectPtr IA_Paste; + // ---- Modifier key state ---- bool bAltHeld = false; bool bCtrlHeld = false; @@ -204,6 +213,9 @@ private: void HandleSnapToGround(const FInputActionValue& Value); void HandleUndo(const FInputActionValue& Value); void HandleRedo(const FInputActionValue& Value); + void HandleDuplicate(const FInputActionValue& Value); + void HandleCopy(const FInputActionValue& Value); + void HandlePaste(const FInputActionValue& Value); // ---- Helpers ---- void CreateInputActions(); diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp index ecf17a3..8136f40 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp @@ -2,6 +2,7 @@ #include "PS_Editor_Gizmo.h" #include "PS_Editor_UndoManager.h" #include "PS_Editor_PlayerController.h" +#include "PS_Editor_SpawnManager.h" #include "GameFramework/PlayerController.h" #include "Engine/World.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(PC); + if (!EditorPC) return; + + static const FVector DuplicateOffset(50.0f, 50.0f, 0.0f); + TArray NewActors; + + for (const TWeakObjectPtr& Weak : SelectedActors) + { + AActor* Original = Weak.Get(); + if (!Original) continue; + + FActorSpawnParameters SpawnParams; + SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn; + + AActor* NewActor = PC->GetWorld()->SpawnActor( + 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 Action = MakeShared(); + 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& 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(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 NewActors; + + for (const FPS_Editor_ClipboardEntry& Entry : Clipboard) + { + if (!Entry.ActorClass) continue; + + FActorSpawnParameters SpawnParams; + SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn; + + AActor* NewActor = PC->GetWorld()->SpawnActor( + 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 Action = MakeShared(); + 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() { SelectedActors.RemoveAll([](const TWeakObjectPtr& Weak) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.h index 60c65aa..e2ece7c 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.h @@ -8,6 +8,17 @@ class APlayerController; 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); /** @@ -39,6 +50,18 @@ public: /** Snap all selected actors to the ground below them. */ 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 ---- const TArray>& GetSelectedActors() const { return SelectedActors; } @@ -95,6 +118,9 @@ private: EPS_Editor_TransformMode CurrentTransformMode = EPS_Editor_TransformMode::Translate; EPS_Editor_Space CurrentSpace = EPS_Editor_Space::World; + /** Clipboard for copy/paste. */ + TArray Clipboard; + /** Toggle custom depth stencil rendering on an actor's primitive components. */ void SetActorHighlight(AActor* Actor, bool bHighlight); diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.h index 03b0848..c85af53 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.h @@ -119,6 +119,7 @@ struct FPS_Editor_SpawnAction : public FPS_Editor_Action FPS_Editor_SpawnAction() : FPS_Editor_Action(EPS_Editor_ActionType::Spawn) {} TArray> Actors; + FString Description; // Optional override (e.g. "Duplicate", "Paste") virtual void Execute() override { @@ -150,6 +151,7 @@ struct FPS_Editor_SpawnAction : public FPS_Editor_Action virtual FString GetDescription() const override { + if (!Description.IsEmpty()) return Description; return FString::Printf(TEXT("Spawn %d actor(s)"), Actors.Num()); } }; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp index 84ccec2..bde80aa 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp @@ -203,6 +203,14 @@ void UPS_Editor_SpawnManager::DestroyAllSpawnedActors() UE_LOG(LogTemp, Log, TEXT("PS_Editor: All spawned actors destroyed")); } +void UPS_Editor_SpawnManager::TrackSpawnedActor(AActor* Actor) +{ + if (Actor) + { + SpawnedActors.Add(Actor); + } +} + TArray UPS_Editor_SpawnManager::GetCategories() const { TArray Categories; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h index b45321e..86a7041 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h @@ -60,6 +60,9 @@ public: /** Get unique categories for filtering. */ TArray GetCategories() const; + /** Register an externally-spawned actor for tracking (used by duplicate/paste). */ + void TrackSpawnedActor(AActor* Actor); + private: UPROPERTY() TWeakObjectPtr OwnerPC;