Unify spline creation/editing into single seamless mode
Major UX simplification: - Removed SplineEditing mode — single SplinePlacement mode handles both creation and editing with identical behavior - Actor spawned and tracked on first click (no "finish" step needed) - Each point add/insert/move has individual undo - Right-click always = camera fly (no special handling during creation) - Escape/Done just exits edit mode, nothing to "finish" Catalog system: - MasterCatalog DataAsset groups multiple SpawnCatalogs (Tools, Characters, Props) - PlayerController references one MasterCatalog instead of single catalog - SpawnManager.AddCatalog() loads sub-catalogs incrementally - Factory for MasterCatalog creation in Content Browser Spline polish: - SplineActor has built-in SpawnableComponent + EditableComponent - SplineColor/SplineSelectedColor/SplineTag as UPROPERTY for BP variants - All spline point operations use World coordinate space (fixes snap-to-ground) - Snap-to-ground ignores gizmo actor (was hitting gizmo guide lines) - Tube mesh collision forced with UpdateBounds after CreateMeshSection - Tube insert and handle selection include active placement actor (not just selected) - Notification bar shows contextual Delete Point button when CP selected Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Unreal/Plugins/PS_Editor/Content/PS_MasterCatalog.uasset
Normal file
BIN
Unreal/Plugins/PS_Editor/Content/PS_MasterCatalog.uasset
Normal file
Binary file not shown.
BIN
Unreal/Plugins/PS_Editor/Content/PS_PropsCatalog.uasset
Normal file
BIN
Unreal/Plugins/PS_Editor/Content/PS_PropsCatalog.uasset
Normal file
Binary file not shown.
BIN
Unreal/Plugins/PS_Editor/Content/PS_SplineEnnemy.uasset
Normal file
BIN
Unreal/Plugins/PS_Editor/Content/PS_SplineEnnemy.uasset
Normal file
Binary file not shown.
BIN
Unreal/Plugins/PS_Editor/Content/PS_ToolsCatalog.uasset
Normal file
BIN
Unreal/Plugins/PS_Editor/Content/PS_ToolsCatalog.uasset
Normal file
Binary file not shown.
@@ -577,19 +577,34 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for spline point handle hit → select point, move gizmo to it (only in SplineEditing mode)
|
// Check for spline point handle hit → select point, move gizmo to it (SplineEditing or SplinePlacement)
|
||||||
if (APS_Editor_PlayerController* EdPC = Cast<APS_Editor_PlayerController>(GetController()))
|
if (APS_Editor_PlayerController* EdPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
{
|
{
|
||||||
if (EdPC->GetEditorMode() == EPS_Editor_EditorMode::SplineEditing)
|
const EPS_Editor_EditorMode Mode = EdPC->GetEditorMode();
|
||||||
|
if (Mode == EPS_Editor_EditorMode::SplinePlacement || Mode == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
{
|
{
|
||||||
|
// Collect candidate splines: selected actors + active placement actor
|
||||||
|
TArray<APS_Editor_SplineActor*> CandidateSplines;
|
||||||
if (UPS_Editor_SelectionManager* SM = EdPC->GetSelectionManager())
|
if (UPS_Editor_SelectionManager* SM = EdPC->GetSelectionManager())
|
||||||
{
|
{
|
||||||
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||||
{
|
{
|
||||||
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Weak.Get()))
|
if (APS_Editor_SplineActor* S = Cast<APS_Editor_SplineActor>(Weak.Get()))
|
||||||
|
{
|
||||||
|
CandidateSplines.AddUnique(S);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (APS_Editor_SplineActor* ActiveSpline = Cast<APS_Editor_SplineActor>(EdPC->GetActivePlacementActor()))
|
||||||
|
{
|
||||||
|
CandidateSplines.AddUnique(ActiveSpline);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (APS_Editor_SplineActor* Spline : CandidateSplines)
|
||||||
|
{
|
||||||
|
// Center cube check (skip in SplinePlacement — no center cube interaction during creation)
|
||||||
|
if (Mode == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
{
|
{
|
||||||
// First check: did we hit the center cube? If so, skip handle detection
|
|
||||||
// (let the click fall through to normal selection → gizmo at actor pivot)
|
|
||||||
FHitResult SplineHit;
|
FHitResult SplineHit;
|
||||||
FCollisionQueryParams SplineParams;
|
FCollisionQueryParams SplineParams;
|
||||||
SplineParams.AddIgnoredActor(this);
|
SplineParams.AddIgnoredActor(this);
|
||||||
@@ -598,13 +613,13 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
|||||||
{
|
{
|
||||||
if (Spline->IsCenterCube(SplineHit.GetComponent()))
|
if (Spline->IsCenterCube(SplineHit.GetComponent()))
|
||||||
{
|
{
|
||||||
// Center cube hit → reset point selection, let normal flow handle it
|
|
||||||
ActiveSplinePointIndex = -1;
|
ActiveSplinePointIndex = -1;
|
||||||
DraggedSplineActor = nullptr;
|
DraggedSplineActor = nullptr;
|
||||||
Spline->ClearHandleHighlight();
|
Spline->ClearHandleHighlight();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const int32 HandleIdx = Spline->GetHandleIndexUnderCursor(RayOrigin, RayDir);
|
const int32 HandleIdx = Spline->GetHandleIndexUnderCursor(RayOrigin, RayDir);
|
||||||
if (HandleIdx >= 0)
|
if (HandleIdx >= 0)
|
||||||
@@ -613,26 +628,42 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
|||||||
DraggedSplineActor = Spline;
|
DraggedSplineActor = Spline;
|
||||||
Spline->HighlightHandle(HandleIdx);
|
Spline->HighlightHandle(HandleIdx);
|
||||||
|
|
||||||
|
if (UPS_Editor_SelectionManager* SM = EdPC->GetSelectionManager())
|
||||||
|
{
|
||||||
if (APS_Editor_Gizmo* Gizmo = SM->GetGizmo())
|
if (APS_Editor_Gizmo* Gizmo = SM->GetGizmo())
|
||||||
{
|
{
|
||||||
Gizmo->SetActorLocation(Spline->GetPointLocation(HandleIdx));
|
// Force gizmo active at this point (even if spline not in SelectionManager yet)
|
||||||
|
Gizmo->SetGizmoActive(true, Spline->GetPointLocation(HandleIdx));
|
||||||
}
|
}
|
||||||
|
|
||||||
SM->SetTransformMode(EPS_Editor_TransformMode::Translate);
|
SM->SetTransformMode(EPS_Editor_TransformMode::Translate);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// In SplineEditing mode: click on spline tube mesh → insert a CP
|
// In SplinePlacement mode: click on spline tube mesh → insert a CP
|
||||||
if (APS_Editor_PlayerController* EdPC2 = Cast<APS_Editor_PlayerController>(GetController()))
|
if (APS_Editor_PlayerController* EdPC2 = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
{
|
{
|
||||||
if (EdPC2->GetEditorMode() == EPS_Editor_EditorMode::SplineEditing)
|
if (EdPC2->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
{
|
{
|
||||||
|
// Collect candidate splines: selected + active placement actor
|
||||||
|
TArray<APS_Editor_SplineActor*> TubeCandidates;
|
||||||
if (UPS_Editor_SelectionManager* SM2 = EdPC2->GetSelectionManager())
|
if (UPS_Editor_SelectionManager* SM2 = EdPC2->GetSelectionManager())
|
||||||
|
{
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SM2->GetSelectedActors())
|
||||||
|
{
|
||||||
|
if (APS_Editor_SplineActor* S = Cast<APS_Editor_SplineActor>(Weak.Get()))
|
||||||
|
TubeCandidates.AddUnique(S);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (APS_Editor_SplineActor* ActiveSpline = Cast<APS_Editor_SplineActor>(EdPC2->GetActivePlacementActor()))
|
||||||
|
{
|
||||||
|
TubeCandidates.AddUnique(ActiveSpline);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TubeCandidates.Num() > 0)
|
||||||
{
|
{
|
||||||
FHitResult LineHit;
|
FHitResult LineHit;
|
||||||
FCollisionQueryParams LineParams;
|
FCollisionQueryParams LineParams;
|
||||||
@@ -641,10 +672,8 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
|||||||
|
|
||||||
if (GetWorld()->LineTraceSingleByChannel(LineHit, RayOrigin, TraceEnd, ECC_Visibility, LineParams))
|
if (GetWorld()->LineTraceSingleByChannel(LineHit, RayOrigin, TraceEnd, ECC_Visibility, LineParams))
|
||||||
{
|
{
|
||||||
for (const TWeakObjectPtr<AActor>& Weak : SM2->GetSelectedActors())
|
for (APS_Editor_SplineActor* Spline : TubeCandidates)
|
||||||
{
|
{
|
||||||
APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Weak.Get());
|
|
||||||
if (!Spline) continue;
|
|
||||||
|
|
||||||
if (Spline->IsSplineMesh(LineHit.GetComponent()))
|
if (Spline->IsSplineMesh(LineHit.GetComponent()))
|
||||||
{
|
{
|
||||||
@@ -668,11 +697,14 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
|||||||
DraggedSplineActor = Spline;
|
DraggedSplineActor = Spline;
|
||||||
Spline->HighlightHandle(InsertIdx);
|
Spline->HighlightHandle(InsertIdx);
|
||||||
|
|
||||||
if (APS_Editor_Gizmo* G = SM2->GetGizmo())
|
if (UPS_Editor_SelectionManager* InsertSM = EdPC2->GetSelectionManager())
|
||||||
{
|
{
|
||||||
G->SetActorLocation(Spline->GetPointLocation(InsertIdx));
|
if (APS_Editor_Gizmo* G = InsertSM->GetGizmo())
|
||||||
|
{
|
||||||
|
G->SetGizmoActive(true, Spline->GetPointLocation(InsertIdx));
|
||||||
|
}
|
||||||
|
InsertSM->SetTransformMode(EPS_Editor_TransformMode::Translate);
|
||||||
}
|
}
|
||||||
SM2->SetTransformMode(EPS_Editor_TransformMode::Translate);
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Inserted spline point at index %d"), InsertIdx);
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Inserted spline point at index %d"), InsertIdx);
|
||||||
return;
|
return;
|
||||||
@@ -683,6 +715,63 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In SplineEditing mode: click outside tube/handles → add point at end
|
||||||
|
if (APS_Editor_PlayerController* EdPC3 = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
|
{
|
||||||
|
if (EdPC3->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
|
{
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(EdPC3->GetActivePlacementActor()))
|
||||||
|
{
|
||||||
|
// Raycast to get world click position
|
||||||
|
FHitResult AddHit;
|
||||||
|
FCollisionQueryParams AddParams;
|
||||||
|
AddParams.AddIgnoredActor(this);
|
||||||
|
AddParams.AddIgnoredActor(Spline);
|
||||||
|
const FVector AddEnd = RayOrigin + RayDir * 100000.0f;
|
||||||
|
|
||||||
|
FVector ClickPos;
|
||||||
|
if (GetWorld()->LineTraceSingleByChannel(AddHit, RayOrigin, AddEnd, ECC_Visibility, AddParams))
|
||||||
|
{
|
||||||
|
ClickPos = AddHit.ImpactPoint + FVector(0.0f, 0.0f, 5.0f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ClickPos = RayOrigin + RayDir * 1000.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FVector> OldPoints = Spline->GetAllPointLocations();
|
||||||
|
Spline->AddPoint(ClickPos);
|
||||||
|
|
||||||
|
if (UPS_Editor_UndoManager* UM = EdPC3->GetUndoManager())
|
||||||
|
{
|
||||||
|
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
|
||||||
|
Action->SplineActor = Spline;
|
||||||
|
Action->OldPoints = OldPoints;
|
||||||
|
Action->NewPoints = Spline->GetAllPointLocations();
|
||||||
|
Action->Description = TEXT("Add spline point");
|
||||||
|
UM->RecordAction(Action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select the new point
|
||||||
|
const int32 NewIdx = Spline->GetNumPoints() - 1;
|
||||||
|
ActiveSplinePointIndex = NewIdx;
|
||||||
|
DraggedSplineActor = Spline;
|
||||||
|
Spline->HighlightHandle(NewIdx);
|
||||||
|
|
||||||
|
if (UPS_Editor_SelectionManager* SM3 = EdPC3->GetSelectionManager())
|
||||||
|
{
|
||||||
|
if (APS_Editor_Gizmo* G = SM3->GetGizmo())
|
||||||
|
{
|
||||||
|
G->SetActorLocation(Spline->GetPointLocation(NewIdx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Added spline point at end (index %d)"), NewIdx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clicking elsewhere resets spline point selection
|
// Clicking elsewhere resets spline point selection
|
||||||
ActiveSplinePointIndex = -1;
|
ActiveSplinePointIndex = -1;
|
||||||
DraggedSplineActor = nullptr;
|
DraggedSplineActor = nullptr;
|
||||||
@@ -809,15 +898,6 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
|
|||||||
|
|
||||||
void APS_Editor_Pawn::HandleRightClickStarted(const FInputActionValue& Value)
|
void APS_Editor_Pawn::HandleRightClickStarted(const FInputActionValue& Value)
|
||||||
{
|
{
|
||||||
// In spline placement mode, right-click finishes the spline
|
|
||||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
|
||||||
{
|
|
||||||
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
|
||||||
{
|
|
||||||
EditorPC->FinishPointPlacement();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SetCameraMode(EPS_Editor_CameraMode::Fly);
|
SetCameraMode(EPS_Editor_CameraMode::Fly);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -889,7 +969,7 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
|||||||
if (!EditorPC) return;
|
if (!EditorPC) return;
|
||||||
|
|
||||||
// In SplineEditing mode with a point selected: delete that point
|
// In SplineEditing mode with a point selected: delete that point
|
||||||
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplineEditing
|
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement
|
||||||
&& ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
|
&& ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
|
||||||
{
|
{
|
||||||
APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get());
|
APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get());
|
||||||
@@ -1005,21 +1085,34 @@ void APS_Editor_Pawn::HandleSnapToGround(const FInputActionValue& Value)
|
|||||||
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
|
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
{
|
{
|
||||||
// If a spline point is selected, snap that point to ground
|
// If a spline point is selected (in edit or placement mode), snap that point to ground
|
||||||
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
|
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
|
||||||
{
|
{
|
||||||
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get()))
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get()))
|
||||||
{
|
{
|
||||||
const FVector PointLoc = Spline->GetPointLocation(ActiveSplinePointIndex);
|
const FVector PointLoc = Spline->GetPointLocation(ActiveSplinePointIndex);
|
||||||
const FVector TraceStart = PointLoc + FVector(0.0f, 0.0f, 500.0f);
|
const FVector TraceStart = PointLoc + FVector(0.0f, 0.0f, 1.0f); // Just above current position
|
||||||
const FVector TraceEnd = PointLoc - FVector(0.0f, 0.0f, 10000.0f);
|
const FVector TraceEnd = PointLoc - FVector(0.0f, 0.0f, 10000.0f);
|
||||||
|
|
||||||
FHitResult Hit;
|
FHitResult Hit;
|
||||||
FCollisionQueryParams Params;
|
FCollisionQueryParams Params;
|
||||||
Params.AddIgnoredActor(Spline);
|
Params.AddIgnoredActor(Spline);
|
||||||
|
Params.AddIgnoredActor(this);
|
||||||
|
if (UPS_Editor_SelectionManager* SnapSM = EditorPC->GetSelectionManager())
|
||||||
|
{
|
||||||
|
if (APS_Editor_Gizmo* SnapGizmo = SnapSM->GetGizmo())
|
||||||
|
{
|
||||||
|
Params.AddIgnoredActor(SnapGizmo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (GetWorld()->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_Visibility, Params))
|
if (GetWorld()->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_Visibility, Params))
|
||||||
{
|
{
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: SnapToGround - PointLoc=%s, HitZ=%.2f, HitActor=%s, HitComp=%s"),
|
||||||
|
*PointLoc.ToString(), Hit.ImpactPoint.Z,
|
||||||
|
Hit.GetActor() ? *Hit.GetActor()->GetName() : TEXT("null"),
|
||||||
|
Hit.GetComponent() ? *Hit.GetComponent()->GetName() : TEXT("null"));
|
||||||
|
|
||||||
// Store state for undo
|
// Store state for undo
|
||||||
TArray<FVector> OldPoints = Spline->GetAllPointLocations();
|
TArray<FVector> OldPoints = Spline->GetAllPointLocations();
|
||||||
|
|
||||||
@@ -1103,21 +1196,23 @@ void APS_Editor_Pawn::HandleSplineMode(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// If a spline is selected, append points to it instead of creating a new one
|
// S key only appends to a selected point-placeable actor (no new creation from shortcut)
|
||||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||||
{
|
{
|
||||||
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||||
{
|
{
|
||||||
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Weak.Get()))
|
if (AActor* Actor = Weak.Get())
|
||||||
|
{
|
||||||
|
if (Actor->Implements<UPS_Editor_PointPlaceable>())
|
||||||
{
|
{
|
||||||
ActiveSplinePointIndex = -1;
|
ActiveSplinePointIndex = -1;
|
||||||
DraggedSplineActor = nullptr;
|
DraggedSplineActor = nullptr;
|
||||||
EditorPC->BeginPointPlacementAppend(Spline);
|
EditorPC->BeginPointPlacementAppend(Actor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EditorPC->BeginPointPlacement();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1141,7 +1236,7 @@ void APS_Editor_Pawn::HandleCancel(const FInputActionValue& Value)
|
|||||||
{
|
{
|
||||||
EditorPC->CancelPointPlacement();
|
EditorPC->CancelPointPlacement();
|
||||||
}
|
}
|
||||||
else if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplineEditing)
|
else if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
{
|
{
|
||||||
EditorPC->FinishSplineEditing();
|
EditorPC->FinishSplineEditing();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "PS_Editor_SelectionManager.h"
|
#include "PS_Editor_SelectionManager.h"
|
||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
#include "PS_Editor_SpawnManager.h"
|
#include "PS_Editor_SpawnManager.h"
|
||||||
|
#include "PS_Editor_SpawnCatalog.h"
|
||||||
#include "PS_Editor_SceneSerializer.h"
|
#include "PS_Editor_SceneSerializer.h"
|
||||||
#include "PS_Editor_Gizmo.h"
|
#include "PS_Editor_Gizmo.h"
|
||||||
#include "PS_Editor_Pawn.h"
|
#include "PS_Editor_Pawn.h"
|
||||||
@@ -26,10 +27,26 @@ void APS_Editor_PlayerController::BeginPlay()
|
|||||||
|
|
||||||
UndoManager = NewObject<UPS_Editor_UndoManager>(this);
|
UndoManager = NewObject<UPS_Editor_UndoManager>(this);
|
||||||
|
|
||||||
// Load spawn catalog and create spawn manager
|
// Create spawn manager and load catalogs from MasterCatalog
|
||||||
SpawnManager = NewObject<UPS_Editor_SpawnManager>(this);
|
SpawnManager = NewObject<UPS_Editor_SpawnManager>(this);
|
||||||
UPS_Editor_SpawnCatalog* LoadedCatalog = SpawnCatalogAsset.LoadSynchronous();
|
SpawnManager->Initialize(this);
|
||||||
SpawnManager->Initialize(this, LoadedCatalog);
|
|
||||||
|
if (UPS_Editor_MasterCatalog* Master = MasterCatalogAsset.LoadSynchronous())
|
||||||
|
{
|
||||||
|
for (const TSoftObjectPtr<UPS_Editor_SpawnCatalog>& CatalogRef : Master->Catalogs)
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SpawnCatalog* Catalog = CatalogRef.LoadSynchronous())
|
||||||
|
{
|
||||||
|
SpawnManager->AddCatalog(Catalog);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: MasterCatalog loaded with %d sub-catalogs, %d total entries"),
|
||||||
|
Master->Catalogs.Num(), SpawnManager->GetResolvedEntries().Num());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: No MasterCatalog assigned."));
|
||||||
|
}
|
||||||
|
|
||||||
SceneSerializer = NewObject<UPS_Editor_SceneSerializer>(this);
|
SceneSerializer = NewObject<UPS_Editor_SceneSerializer>(this);
|
||||||
SceneSerializer->SetSelectionManager(SelectionManager);
|
SceneSerializer->SetSelectionManager(SelectionManager);
|
||||||
@@ -121,67 +138,21 @@ void APS_Editor_PlayerController::BeginPointPlacementAppend(AActor* ExistingActo
|
|||||||
|
|
||||||
void APS_Editor_PlayerController::FinishPointPlacement()
|
void APS_Editor_PlayerController::FinishPointPlacement()
|
||||||
{
|
{
|
||||||
IPS_Editor_PointPlaceable* Placeable = Cast<IPS_Editor_PointPlaceable>(ActivePlacementActor);
|
// Just exit the mode — actor is already tracked, all edits already have undo
|
||||||
if (!ActivePlacementActor || !Placeable || Placeable->GetCurrentPointCount() < Placeable->GetMinimumPoints())
|
|
||||||
{
|
|
||||||
CancelPointPlacement();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Placeable->OnPlacementFinished();
|
|
||||||
|
|
||||||
if (bAppendingToActor)
|
|
||||||
{
|
|
||||||
// Appending: record undo for the added points
|
|
||||||
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(ActivePlacementActor))
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(ActivePlacementActor))
|
||||||
{
|
{
|
||||||
Spline->SetHandlesVisible(false);
|
Spline->SetHandlesVisible(false);
|
||||||
if (UndoManager)
|
Spline->ClearHandleHighlight();
|
||||||
{
|
|
||||||
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
|
|
||||||
Action->SplineActor = Spline;
|
|
||||||
Action->OldPoints = AppendInitialPoints;
|
|
||||||
Action->NewPoints = Spline->GetAllPointLocations();
|
|
||||||
Action->Description = FString::Printf(TEXT("Add %d point(s)"),
|
|
||||||
Spline->GetNumPoints() - AppendInitialPoints.Num());
|
|
||||||
UndoManager->RecordAction(Action);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (SelectionManager)
|
if (ActivePlacementActor && SelectionManager)
|
||||||
{
|
{
|
||||||
SelectionManager->ClearSelection();
|
SelectionManager->ClearSelection();
|
||||||
SelectionManager->SelectActor(ActivePlacementActor);
|
SelectionManager->SelectActor(ActivePlacementActor);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// New actor: track and record spawn undo
|
|
||||||
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(ActivePlacementActor))
|
|
||||||
{
|
|
||||||
Spline->SetHandlesVisible(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SpawnManager)
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Exited placement mode"));
|
||||||
{
|
|
||||||
SpawnManager->TrackSpawnedActor(ActivePlacementActor);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (UndoManager)
|
|
||||||
{
|
|
||||||
TSharedPtr<FPS_Editor_SpawnAction> Action = MakeShared<FPS_Editor_SpawnAction>();
|
|
||||||
Action->Actors.Add(ActivePlacementActor);
|
|
||||||
Action->Description = FString::Printf(TEXT("Create %s (%d points)"),
|
|
||||||
*ActivePlacementActor->GetClass()->GetName(), Placeable->GetCurrentPointCount());
|
|
||||||
UndoManager->RecordAction(Action);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SelectionManager)
|
|
||||||
{
|
|
||||||
SelectionManager->SelectActor(ActivePlacementActor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Placement finished (%d points)"), Placeable->GetCurrentPointCount());
|
|
||||||
ActivePlacementActor = nullptr;
|
ActivePlacementActor = nullptr;
|
||||||
bAppendingToActor = false;
|
bAppendingToActor = false;
|
||||||
AppendInitialPoints.Empty();
|
AppendInitialPoints.Empty();
|
||||||
@@ -190,57 +161,8 @@ void APS_Editor_PlayerController::FinishPointPlacement()
|
|||||||
|
|
||||||
void APS_Editor_PlayerController::CancelPointPlacement()
|
void APS_Editor_PlayerController::CancelPointPlacement()
|
||||||
{
|
{
|
||||||
if (ActivePlacementActor)
|
// Same as finish — just exit mode. Undo handles reverting if needed.
|
||||||
{
|
FinishPointPlacement();
|
||||||
IPS_Editor_PointPlaceable* Placeable = Cast<IPS_Editor_PointPlaceable>(ActivePlacementActor);
|
|
||||||
if (Placeable) Placeable->OnPlacementCancelled();
|
|
||||||
|
|
||||||
if (bAppendingToActor)
|
|
||||||
{
|
|
||||||
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(ActivePlacementActor))
|
|
||||||
{
|
|
||||||
Spline->SetAllPointLocations(AppendInitialPoints);
|
|
||||||
Spline->SetHandlesVisible(false);
|
|
||||||
}
|
|
||||||
if (SelectionManager)
|
|
||||||
{
|
|
||||||
SelectionManager->ClearSelection();
|
|
||||||
SelectionManager->SelectActor(ActivePlacementActor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ActivePlacementActor->Destroy();
|
|
||||||
}
|
|
||||||
ActivePlacementActor = nullptr;
|
|
||||||
}
|
|
||||||
bAppendingToActor = false;
|
|
||||||
AppendInitialPoints.Empty();
|
|
||||||
EditorMode = EPS_Editor_EditorMode::Normal;
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Placement cancelled"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void APS_Editor_PlayerController::BeginSplineEditing(APS_Editor_SplineActor* Spline)
|
|
||||||
{
|
|
||||||
if (!Spline) return;
|
|
||||||
|
|
||||||
ActivePlacementActor = Spline;
|
|
||||||
EditorMode = EPS_Editor_EditorMode::SplineEditing;
|
|
||||||
Spline->SetHandlesVisible(true);
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline editing mode started (%d points)"), Spline->GetNumPoints());
|
|
||||||
}
|
|
||||||
|
|
||||||
void APS_Editor_PlayerController::FinishSplineEditing()
|
|
||||||
{
|
|
||||||
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(ActivePlacementActor))
|
|
||||||
{
|
|
||||||
Spline->SetHandlesVisible(false);
|
|
||||||
Spline->ClearHandleHighlight();
|
|
||||||
}
|
|
||||||
ActivePlacementActor = nullptr;
|
|
||||||
EditorMode = EPS_Editor_EditorMode::Normal;
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline editing mode ended"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bAdditive)
|
void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bAdditive)
|
||||||
@@ -270,19 +192,77 @@ void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bA
|
|||||||
|
|
||||||
if (!ActivePlacementActor)
|
if (!ActivePlacementActor)
|
||||||
{
|
{
|
||||||
// First click: spawn the actor
|
// First click: spawn the actor, track immediately, record undo
|
||||||
FActorSpawnParameters SpawnParams;
|
FActorSpawnParameters SpawnParams;
|
||||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||||
UClass* ClassToSpawn = PlacementActorClass ? PlacementActorClass.Get() : APS_Editor_SplineActor::StaticClass();
|
UClass* ClassToSpawn = PlacementActorClass ? PlacementActorClass.Get() : APS_Editor_SplineActor::StaticClass();
|
||||||
ActivePlacementActor = GetWorld()->SpawnActor<AActor>(ClassToSpawn, ClickWorldPos, FRotator::ZeroRotator, SpawnParams);
|
ActivePlacementActor = GetWorld()->SpawnActor<AActor>(ClassToSpawn, ClickWorldPos, FRotator::ZeroRotator, SpawnParams);
|
||||||
|
|
||||||
|
if (ActivePlacementActor)
|
||||||
|
{
|
||||||
|
// Track immediately (for outliner, save/load, scene clear)
|
||||||
|
if (SpawnManager)
|
||||||
|
{
|
||||||
|
SpawnManager->TrackSpawnedActor(ActivePlacementActor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add point via the interface
|
// Record spawn undo immediately
|
||||||
if (IPS_Editor_PointPlaceable* Placeable = Cast<IPS_Editor_PointPlaceable>(ActivePlacementActor))
|
if (UndoManager)
|
||||||
|
{
|
||||||
|
TSharedPtr<FPS_Editor_SpawnAction> Action = MakeShared<FPS_Editor_SpawnAction>();
|
||||||
|
Action->Actors.Add(ActivePlacementActor);
|
||||||
|
Action->Description = TEXT("Create placeable actor");
|
||||||
|
UndoManager->RecordAction(Action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we're "appending" to the tracked actor
|
||||||
|
bAppendingToActor = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we clicked on the existing spline tube → insert instead of append
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(ActivePlacementActor))
|
||||||
|
{
|
||||||
|
if (Spline->GetNumPoints() >= 2)
|
||||||
|
{
|
||||||
|
// Raycast to check if we hit the tube mesh
|
||||||
|
FHitResult TubeHit;
|
||||||
|
FCollisionQueryParams TubeParams;
|
||||||
|
TubeParams.AddIgnoredActor(GetPawn());
|
||||||
|
const FVector TubeEnd = WorldOrigin + WorldDirection * 100000.0f;
|
||||||
|
|
||||||
|
if (GetWorld()->LineTraceSingleByChannel(TubeHit, WorldOrigin, TubeEnd, ECC_Visibility, TubeParams))
|
||||||
|
{
|
||||||
|
if (Spline->IsSplineMesh(TubeHit.GetComponent()))
|
||||||
|
{
|
||||||
|
const int32 InsertIdx = Spline->FindInsertIndex(TubeHit.ImpactPoint);
|
||||||
|
Spline->InsertPoint(InsertIdx, TubeHit.ImpactPoint + FVector(0.0f, 0.0f, 5.0f));
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Inserted point at index %d during placement"), InsertIdx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: add point at end via the interface (with undo)
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(ActivePlacementActor))
|
||||||
|
{
|
||||||
|
TArray<FVector> OldPoints = Spline->GetAllPointLocations();
|
||||||
|
Spline->AddPoint(ClickWorldPos);
|
||||||
|
|
||||||
|
if (UndoManager)
|
||||||
|
{
|
||||||
|
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
|
||||||
|
Action->SplineActor = Spline;
|
||||||
|
Action->OldPoints = OldPoints;
|
||||||
|
Action->NewPoints = Spline->GetAllPointLocations();
|
||||||
|
Action->Description = TEXT("Add spline point");
|
||||||
|
UndoManager->RecordAction(Action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (IPS_Editor_PointPlaceable* Placeable = Cast<IPS_Editor_PointPlaceable>(ActivePlacementActor))
|
||||||
{
|
{
|
||||||
Placeable->AddPlacementPoint(ClickWorldPos);
|
Placeable->AddPlacementPoint(ClickWorldPos);
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Point %d added at %s"),
|
|
||||||
Placeable->GetCurrentPointCount(), *ClickWorldPos.ToString());
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class UPS_Editor_SelectionManager;
|
|||||||
class UPS_Editor_UndoManager;
|
class UPS_Editor_UndoManager;
|
||||||
class UPS_Editor_SpawnManager;
|
class UPS_Editor_SpawnManager;
|
||||||
class UPS_Editor_SpawnCatalog;
|
class UPS_Editor_SpawnCatalog;
|
||||||
|
class UPS_Editor_MasterCatalog;
|
||||||
class UPS_Editor_SceneSerializer;
|
class UPS_Editor_SceneSerializer;
|
||||||
class APS_Editor_SplineActor;
|
class APS_Editor_SplineActor;
|
||||||
class IPS_Editor_PointPlaceable;
|
class IPS_Editor_PointPlaceable;
|
||||||
@@ -37,9 +38,9 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||||
UPS_Editor_SceneSerializer* GetSceneSerializer() const { return SceneSerializer; }
|
UPS_Editor_SceneSerializer* GetSceneSerializer() const { return SceneSerializer; }
|
||||||
|
|
||||||
/** The spawn catalog DataAsset to load. Set this in the Blueprint or World Settings. */
|
/** Master catalog grouping all sub-catalogs (Tools, Characters, Props...). */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||||
TSoftObjectPtr<UPS_Editor_SpawnCatalog> SpawnCatalogAsset;
|
TSoftObjectPtr<UPS_Editor_MasterCatalog> MasterCatalogAsset;
|
||||||
|
|
||||||
// ---- Editor Mode (Normal / SplinePlacement) ----
|
// ---- Editor Mode (Normal / SplinePlacement) ----
|
||||||
|
|
||||||
@@ -61,14 +62,17 @@ public:
|
|||||||
/** Get the actor currently being placed/edited (nullptr if not in placement mode). */
|
/** Get the actor currently being placed/edited (nullptr if not in placement mode). */
|
||||||
AActor* GetActivePlacementActor() const { return ActivePlacementActor; }
|
AActor* GetActivePlacementActor() const { return ActivePlacementActor; }
|
||||||
|
|
||||||
|
/** True if we're appending to an existing actor (editing), false if creating new. */
|
||||||
|
bool IsAppendingToActor() const { return bAppendingToActor; }
|
||||||
|
|
||||||
// Legacy convenience (for spline-specific code like point editing)
|
// Legacy convenience (for spline-specific code like point editing)
|
||||||
APS_Editor_SplineActor* GetActiveSplineActor() const;
|
APS_Editor_SplineActor* GetActiveSplineActor() const;
|
||||||
|
|
||||||
/** Enter spline point editing mode for a selected spline. */
|
/** Convenience: enter point placement for an existing spline. */
|
||||||
void BeginSplineEditing(APS_Editor_SplineActor* Spline);
|
void BeginSplineEditing(AActor* Spline) { BeginPointPlacementAppend(Spline); }
|
||||||
|
|
||||||
/** Exit spline editing mode. */
|
/** Convenience: exit spline editing. */
|
||||||
void FinishSplineEditing();
|
void FinishSplineEditing() { FinishPointPlacement(); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ void UPS_Editor_SelectionManager::DeselectActor(AActor* Actor)
|
|||||||
{
|
{
|
||||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
|
||||||
{
|
{
|
||||||
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplineEditing && EditorPC->GetActivePlacementActor() == Actor)
|
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement && EditorPC->GetActivePlacementActor() == Actor)
|
||||||
{
|
{
|
||||||
EditorPC->FinishSplineEditing();
|
EditorPC->FinishSplineEditing();
|
||||||
}
|
}
|
||||||
@@ -268,7 +268,7 @@ void UPS_Editor_SelectionManager::ClearSelection()
|
|||||||
{
|
{
|
||||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
|
||||||
{
|
{
|
||||||
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplineEditing)
|
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
{
|
{
|
||||||
EditorPC->FinishSplineEditing();
|
EditorPC->FinishSplineEditing();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,5 @@ UENUM(BlueprintType)
|
|||||||
enum class EPS_Editor_EditorMode : uint8
|
enum class EPS_Editor_EditorMode : uint8
|
||||||
{
|
{
|
||||||
Normal, // Standard select/transform behavior
|
Normal, // Standard select/transform behavior
|
||||||
SplinePlacement, // Clicks add points to current spline
|
SplinePlacement // Creating or editing a point-placeable actor (spline, etc.)
|
||||||
SplineEditing // Editing individual points of a selected spline
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -43,3 +43,19 @@ public:
|
|||||||
void CaptureAllThumbnails();
|
void CaptureAllThumbnails();
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Master catalog that groups multiple SpawnCatalogs.
|
||||||
|
* Assign this to the PlayerController. Each sub-catalog can represent a domain
|
||||||
|
* (Tools, Characters, Props, Vehicles...).
|
||||||
|
*/
|
||||||
|
UCLASS(BlueprintType)
|
||||||
|
class PS_EDITOR_API UPS_Editor_MasterCatalog : public UPrimaryDataAsset
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** Sub-catalogs to load. Each one contributes its entries to the runtime spawn UI. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||||
|
TArray<TSoftObjectPtr<UPS_Editor_SpawnCatalog>> Catalogs;
|
||||||
|
};
|
||||||
|
|||||||
@@ -22,6 +22,25 @@ FText UPS_Editor_SpawnCatalogFactory::GetDisplayName() const
|
|||||||
return LOCTEXT("SpawnCatalogDisplayName", "PS Editor Spawn Catalog");
|
return LOCTEXT("SpawnCatalogDisplayName", "PS Editor Spawn Catalog");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Master Catalog Factory ----
|
||||||
|
|
||||||
|
UPS_Editor_MasterCatalogFactory::UPS_Editor_MasterCatalogFactory()
|
||||||
|
{
|
||||||
|
SupportedClass = UPS_Editor_MasterCatalog::StaticClass();
|
||||||
|
bCreateNew = true;
|
||||||
|
bEditAfterNew = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
UObject* UPS_Editor_MasterCatalogFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
|
||||||
|
{
|
||||||
|
return NewObject<UPS_Editor_MasterCatalog>(InParent, InClass, InName, Flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
FText UPS_Editor_MasterCatalogFactory::GetDisplayName() const
|
||||||
|
{
|
||||||
|
return LOCTEXT("MasterCatalogDisplayName", "PS Editor Master Catalog");
|
||||||
|
}
|
||||||
|
|
||||||
#undef LOCTEXT_NAMESPACE
|
#undef LOCTEXT_NAMESPACE
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -19,4 +19,17 @@ public:
|
|||||||
virtual FText GetDisplayName() const override;
|
virtual FText GetDisplayName() const override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
UCLASS(HideCategories = Object)
|
||||||
|
class UPS_Editor_MasterCatalogFactory : public UFactory
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPS_Editor_MasterCatalogFactory();
|
||||||
|
|
||||||
|
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
|
||||||
|
virtual bool ShouldShowInNewMenu() const override { return true; }
|
||||||
|
virtual FText GetDisplayName() const override;
|
||||||
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -5,32 +5,18 @@
|
|||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "Engine/World.h"
|
#include "Engine/World.h"
|
||||||
|
|
||||||
void UPS_Editor_SpawnManager::Initialize(APlayerController* InOwnerPC, UPS_Editor_SpawnCatalog* InCatalog)
|
void UPS_Editor_SpawnManager::Initialize(APlayerController* InOwnerPC)
|
||||||
{
|
{
|
||||||
OwnerPC = InOwnerPC;
|
OwnerPC = InOwnerPC;
|
||||||
Catalog = InCatalog;
|
|
||||||
|
|
||||||
if (Catalog)
|
|
||||||
{
|
|
||||||
ResolveCatalog();
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: SpawnCatalog loaded with %d entries"), ResolvedEntries.Num());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: No SpawnCatalog assigned. Spawn catalogue will be empty."));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UPS_Editor_SpawnManager::ResolveCatalog()
|
void UPS_Editor_SpawnManager::AddCatalog(UPS_Editor_SpawnCatalog* InCatalog)
|
||||||
{
|
{
|
||||||
ResolvedEntries.Empty();
|
if (!InCatalog) return;
|
||||||
|
|
||||||
if (!Catalog)
|
const int32 BeforeCount = ResolvedEntries.Num();
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const FPS_Editor_SpawnEntry& Entry : Catalog->Entries)
|
for (const FPS_Editor_SpawnEntry& Entry : InCatalog->Entries)
|
||||||
{
|
{
|
||||||
// Synchronously load the class
|
// Synchronously load the class
|
||||||
UClass* LoadedClass = Entry.ActorClass.LoadSynchronous();
|
UClass* LoadedClass = Entry.ActorClass.LoadSynchronous();
|
||||||
@@ -84,6 +70,9 @@ void UPS_Editor_SpawnManager::ResolveCatalog()
|
|||||||
|
|
||||||
ResolvedEntries.Add(Resolved);
|
ResolvedEntries.Add(Resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Added catalog with %d entries (total: %d)"),
|
||||||
|
ResolvedEntries.Num() - BeforeCount, ResolvedEntries.Num());
|
||||||
}
|
}
|
||||||
|
|
||||||
AActor* UPS_Editor_SpawnManager::SpawnFromCatalog(int32 Index)
|
AActor* UPS_Editor_SpawnManager::SpawnFromCatalog(int32 Index)
|
||||||
|
|||||||
@@ -36,8 +36,11 @@ class PS_EDITOR_API UPS_Editor_SpawnManager : public UObject
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/** Initialize with owning controller and load catalog. */
|
/** Initialize with owning controller. */
|
||||||
void Initialize(APlayerController* InOwnerPC, UPS_Editor_SpawnCatalog* InCatalog);
|
void Initialize(APlayerController* InOwnerPC);
|
||||||
|
|
||||||
|
/** Load and add a catalog. Can be called multiple times for multiple catalogs. */
|
||||||
|
void AddCatalog(UPS_Editor_SpawnCatalog* InCatalog);
|
||||||
|
|
||||||
/** Spawn the actor at the given catalog index, positioned in front of the camera. Returns the spawned actor. */
|
/** Spawn the actor at the given catalog index, positioned in front of the camera. Returns the spawned actor. */
|
||||||
AActor* SpawnFromCatalog(int32 Index);
|
AActor* SpawnFromCatalog(int32 Index);
|
||||||
@@ -70,9 +73,6 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TWeakObjectPtr<APlayerController> OwnerPC;
|
TWeakObjectPtr<APlayerController> OwnerPC;
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
TObjectPtr<UPS_Editor_SpawnCatalog> Catalog;
|
|
||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TArray<FPS_Editor_ResolvedEntry> ResolvedEntries;
|
TArray<FPS_Editor_ResolvedEntry> ResolvedEntries;
|
||||||
|
|
||||||
@@ -80,8 +80,6 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TArray<TWeakObjectPtr<AActor>> SpawnedActors;
|
TArray<TWeakObjectPtr<AActor>> SpawnedActors;
|
||||||
|
|
||||||
/** Resolve all catalog entries (load classes, read SpawnableComponent metadata). */
|
|
||||||
void ResolveCatalog();
|
|
||||||
|
|
||||||
/** Compute spawn location in front of the camera. */
|
/** Compute spawn location in front of the camera. */
|
||||||
FVector ComputeSpawnLocation() const;
|
FVector ComputeSpawnLocation() const;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#include "PS_Editor_SplineActor.h"
|
#include "PS_Editor_SplineActor.h"
|
||||||
|
#include "PS_Editor_SpawnableComponent.h"
|
||||||
|
#include "PS_Editor_EditableComponent.h"
|
||||||
#include "Components/SplineComponent.h"
|
#include "Components/SplineComponent.h"
|
||||||
#include "ProceduralMeshComponent.h"
|
#include "ProceduralMeshComponent.h"
|
||||||
#include "Components/StaticMeshComponent.h"
|
#include "Components/StaticMeshComponent.h"
|
||||||
@@ -32,6 +34,17 @@ APS_Editor_SplineActor::APS_Editor_SplineActor()
|
|||||||
// Handle parent
|
// Handle parent
|
||||||
HandleRoot = CreateDefaultSubobject<USceneComponent>(TEXT("HandleRoot"));
|
HandleRoot = CreateDefaultSubobject<USceneComponent>(TEXT("HandleRoot"));
|
||||||
HandleRoot->SetupAttachment(Root);
|
HandleRoot->SetupAttachment(Root);
|
||||||
|
|
||||||
|
// Spawnable: makes this actor appear in the catalogue
|
||||||
|
SpawnableComp = CreateDefaultSubobject<UPS_Editor_SpawnableComponent>(TEXT("PS_Editor_Spawnable"));
|
||||||
|
SpawnableComp->DisplayName = TEXT("Spline");
|
||||||
|
SpawnableComp->Category = TEXT("Tools");
|
||||||
|
|
||||||
|
// Editable: exposes SplineColor and SplineTag in the runtime property panel
|
||||||
|
EditableComp = CreateDefaultSubobject<UPS_Editor_EditableComponent>(TEXT("PS_Editor_Editable"));
|
||||||
|
EditableComp->EditableProperties.Add(FName(TEXT("SplineColor")));
|
||||||
|
EditableComp->EditableProperties.Add(FName(TEXT("SplineSelectedColor")));
|
||||||
|
EditableComp->EditableProperties.Add(FName(TEXT("SplineTag")));
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_SplineActor::BeginPlay()
|
void APS_Editor_SplineActor::BeginPlay()
|
||||||
@@ -100,9 +113,7 @@ void APS_Editor_SplineActor::BeginPlay()
|
|||||||
|
|
||||||
void APS_Editor_SplineActor::AddPoint(const FVector& WorldPosition)
|
void APS_Editor_SplineActor::AddPoint(const FVector& WorldPosition)
|
||||||
{
|
{
|
||||||
// Convert world to local space
|
SplineComp->AddSplinePoint(WorldPosition, ESplineCoordinateSpace::World, true);
|
||||||
const FVector LocalPos = GetActorTransform().InverseTransformPosition(WorldPosition);
|
|
||||||
SplineComp->AddSplinePoint(LocalPos, ESplineCoordinateSpace::Local, true);
|
|
||||||
|
|
||||||
// Create handle
|
// Create handle
|
||||||
UStaticMeshComponent* Handle = CreatePointHandle(WorldPosition);
|
UStaticMeshComponent* Handle = CreatePointHandle(WorldPosition);
|
||||||
@@ -115,8 +126,7 @@ void APS_Editor_SplineActor::MovePoint(int32 Index, const FVector& NewWorldPosit
|
|||||||
{
|
{
|
||||||
if (Index < 0 || Index >= GetNumPoints()) return;
|
if (Index < 0 || Index >= GetNumPoints()) return;
|
||||||
|
|
||||||
const FVector LocalPos = GetActorTransform().InverseTransformPosition(NewWorldPosition);
|
SplineComp->SetLocationAtSplinePoint(Index, NewWorldPosition, ESplineCoordinateSpace::World, true);
|
||||||
SplineComp->SetLocationAtSplinePoint(Index, LocalPos, ESplineCoordinateSpace::Local, true);
|
|
||||||
|
|
||||||
if (PointHandles.IsValidIndex(Index) && PointHandles[Index])
|
if (PointHandles.IsValidIndex(Index) && PointHandles[Index])
|
||||||
{
|
{
|
||||||
@@ -174,8 +184,7 @@ void APS_Editor_SplineActor::SetAllPointLocations(const TArray<FVector>& Points)
|
|||||||
SplineComp->ClearSplinePoints(false);
|
SplineComp->ClearSplinePoints(false);
|
||||||
for (const FVector& WorldPos : Points)
|
for (const FVector& WorldPos : Points)
|
||||||
{
|
{
|
||||||
const FVector LocalPos = GetActorTransform().InverseTransformPosition(WorldPos);
|
SplineComp->AddSplinePoint(WorldPos, ESplineCoordinateSpace::World, false);
|
||||||
SplineComp->AddSplinePoint(LocalPos, ESplineCoordinateSpace::Local, false);
|
|
||||||
}
|
}
|
||||||
SplineComp->UpdateSpline();
|
SplineComp->UpdateSpline();
|
||||||
|
|
||||||
@@ -246,7 +255,11 @@ void APS_Editor_SplineActor::UpdateVisualization()
|
|||||||
}
|
}
|
||||||
|
|
||||||
SplineMeshComp->ClearAllMeshSections();
|
SplineMeshComp->ClearAllMeshSections();
|
||||||
SplineMeshComp->CreateMeshSection(0, Vertices, Triangles, Normals, UVs, TArray<FColor>(), TArray<FProcMeshTangent>(), false);
|
SplineMeshComp->CreateMeshSection(0, Vertices, Triangles, Normals, UVs, TArray<FColor>(), TArray<FProcMeshTangent>(), true);
|
||||||
|
|
||||||
|
// Force collision and bounds update immediately
|
||||||
|
SplineMeshComp->UpdateBounds();
|
||||||
|
SplineMeshComp->MarkRenderStateDirty();
|
||||||
|
|
||||||
// Apply the current material (normal or selected)
|
// Apply the current material (normal or selected)
|
||||||
// Check if handles are visible as a proxy for "selected" state
|
// Check if handles are visible as a proxy for "selected" state
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
class USplineComponent;
|
class USplineComponent;
|
||||||
class UProceduralMeshComponent;
|
class UProceduralMeshComponent;
|
||||||
|
class UPS_Editor_SpawnableComponent;
|
||||||
|
class UPS_Editor_EditableComponent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime spline actor for path/route editing in PS_Editor.
|
* Runtime spline actor for path/route editing in PS_Editor.
|
||||||
@@ -157,6 +159,12 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<UMaterialInstanceDynamic> CenterCubeMat;
|
TObjectPtr<UMaterialInstanceDynamic> CenterCubeMat;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere)
|
||||||
|
TObjectPtr<UPS_Editor_SpawnableComponent> SpawnableComp;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere)
|
||||||
|
TObjectPtr<UPS_Editor_EditableComponent> EditableComp;
|
||||||
|
|
||||||
/** Tube rendering settings. */
|
/** Tube rendering settings. */
|
||||||
float TubeRadius = 2.0f;
|
float TubeRadius = 2.0f;
|
||||||
int32 TubeSegments = 8;
|
int32 TubeSegments = 8;
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
|||||||
{
|
{
|
||||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
{
|
{
|
||||||
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplineEditing)
|
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
{
|
{
|
||||||
EditorPC->FinishSplineEditing();
|
EditorPC->FinishSplineEditing();
|
||||||
}
|
}
|
||||||
@@ -199,7 +199,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
|||||||
{
|
{
|
||||||
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
EditorPC->FinishPointPlacement();
|
EditorPC->FinishPointPlacement();
|
||||||
else if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplineEditing)
|
else if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
EditorPC->FinishSplineEditing();
|
EditorPC->FinishSplineEditing();
|
||||||
}
|
}
|
||||||
return FReply::Handled();
|
return FReply::Handled();
|
||||||
@@ -321,31 +321,6 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildToolbar()
|
|||||||
})
|
})
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
// New Spline button
|
|
||||||
+ SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 2.0f, 0.0f)
|
|
||||||
[
|
|
||||||
SNew(SButton)
|
|
||||||
.OnClicked_Lambda([this]() -> FReply
|
|
||||||
{
|
|
||||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
|
||||||
{
|
|
||||||
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
|
||||||
{
|
|
||||||
EditorPC->FinishPointPlacement();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
EditorPC->BeginPointPlacement();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return FReply::Handled();
|
|
||||||
})
|
|
||||||
[
|
|
||||||
SNew(STextBlock)
|
|
||||||
.Text(FText::FromString(TEXT("New Spline (S)")))
|
|
||||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
|
||||||
]
|
|
||||||
]
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -784,73 +759,6 @@ void UPS_Editor_MainWidget::PopulateSpawnCatalog()
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Tools section (built-in tools) ----
|
|
||||||
SpawnListContainer->AddSlot()
|
|
||||||
.AutoHeight()
|
|
||||||
.Padding(0.0f, 10.0f, 0.0f, 2.0f)
|
|
||||||
[
|
|
||||||
SNew(STextBlock)
|
|
||||||
.Text(FText::FromString(TEXT("Tools")))
|
|
||||||
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 9))
|
|
||||||
.ColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f))
|
|
||||||
];
|
|
||||||
|
|
||||||
// Spline tool
|
|
||||||
SpawnListContainer->AddSlot()
|
|
||||||
.AutoHeight()
|
|
||||||
.Padding(4.0f, 4.0f)
|
|
||||||
[
|
|
||||||
SNew(SButton)
|
|
||||||
.OnClicked_Lambda([this]() -> FReply
|
|
||||||
{
|
|
||||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
|
||||||
{
|
|
||||||
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
|
||||||
{
|
|
||||||
EditorPC->FinishPointPlacement();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// If a spline is selected, append to it
|
|
||||||
if (UPS_Editor_SelectionManager* SelMgr = SelectionManager.Get())
|
|
||||||
{
|
|
||||||
for (const TWeakObjectPtr<AActor>& Weak : SelMgr->GetSelectedActors())
|
|
||||||
{
|
|
||||||
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Weak.Get()))
|
|
||||||
{
|
|
||||||
EditorPC->BeginPointPlacementAppend(Spline);
|
|
||||||
return FReply::Handled();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EditorPC->BeginPointPlacement();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return FReply::Handled();
|
|
||||||
})
|
|
||||||
[
|
|
||||||
SNew(SBox)
|
|
||||||
.MinDesiredWidth(88.0f)
|
|
||||||
[
|
|
||||||
SNew(SVerticalBox)
|
|
||||||
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center)
|
|
||||||
[
|
|
||||||
SNew(STextBlock)
|
|
||||||
.Text(FText::FromString(TEXT("~")))
|
|
||||||
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 28))
|
|
||||||
.ColorAndOpacity(FLinearColor(1.0f, 0.4f, 0.0f))
|
|
||||||
]
|
|
||||||
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 2.0f, 0.0f, 0.0f)
|
|
||||||
[
|
|
||||||
SNew(STextBlock)
|
|
||||||
.Text(FText::FromString(TEXT("Spline")))
|
|
||||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
|
||||||
.Justification(ETextJustify::Center)
|
|
||||||
.ColorAndOpacity(FLinearColor::White)
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UPS_Editor_MainWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
|
void UPS_Editor_MainWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
|
||||||
@@ -914,18 +822,6 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
|
|||||||
// Update help bar text and button visibility
|
// Update help bar text and button visibility
|
||||||
if (CurrentEditorMode == EPS_Editor_EditorMode::SplinePlacement)
|
if (CurrentEditorMode == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
{
|
{
|
||||||
if (HelpBarText.IsValid())
|
|
||||||
{
|
|
||||||
HelpBarText->SetText(FText::FromString(TEXT("SPLINE: LMB to add points | RMB or Enter to finish | Escape to cancel")));
|
|
||||||
HelpBarText->SetColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f));
|
|
||||||
}
|
|
||||||
if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed);
|
|
||||||
if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(EVisibility::Collapsed);
|
|
||||||
if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Visible);
|
|
||||||
}
|
|
||||||
else if (CurrentEditorMode == EPS_Editor_EditorMode::SplineEditing)
|
|
||||||
{
|
|
||||||
// Check if a specific point is selected (for Delete Point button)
|
|
||||||
bool bPointSelected = false;
|
bool bPointSelected = false;
|
||||||
if (APS_Editor_Pawn* Pawn = Cast<APS_Editor_Pawn>(GetOwningPlayerPawn()))
|
if (APS_Editor_Pawn* Pawn = Cast<APS_Editor_Pawn>(GetOwningPlayerPawn()))
|
||||||
{
|
{
|
||||||
@@ -936,11 +832,11 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
|
|||||||
{
|
{
|
||||||
if (bPointSelected)
|
if (bPointSelected)
|
||||||
{
|
{
|
||||||
HelpBarText->SetText(FText::FromString(TEXT("EDIT SPLINE: Drag gizmo to move point | Del: delete point | Click line to insert | Escape: done")));
|
HelpBarText->SetText(FText::FromString(TEXT("SPLINE: Drag gizmo to move | Del: delete point | Click line: insert | Click outside: add | Esc/Done: finish")));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
HelpBarText->SetText(FText::FromString(TEXT("EDIT SPLINE: Click handle to select point | Click line to insert | End: snap to ground | Escape: done")));
|
HelpBarText->SetText(FText::FromString(TEXT("SPLINE: Click to add points | Click handle to select | Click line to insert | End: snap | Esc/Done: finish")));
|
||||||
}
|
}
|
||||||
HelpBarText->SetColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f));
|
HelpBarText->SetColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user