Add runtime spline creation and editing (WIP)
Spline placement: New Spline button (S key) enters placement mode where clicks add control points. Right-click/Enter finishes, Escape cancels. Min 2 points. Spline visualization as cross-shaped quad strip (vertical + horizontal ribbons). Point editing: select spline to show handles, click a handle to position gizmo on it, then use translate gizmo to move individual points. Undo supported via full-state snapshots (SplinePointAction). Visual feedback: auto-generated M_PS_Editor_SplineLine material (unlit opaque with depth test) for normal state. Gizmo material (no depth test) for selected state. Center cube at centroid for whole-spline selection. Handles highlight on selection. Serialization: spline points exported to CustomProperties (PointCount + Point_N). C++ class fallback via FindObject for LoadScene. Spline appears in scene outliner. Still WIP: depth material swap on selection, serialization edge cases, and point editing polish to be refined. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
BIN
Unreal/Plugins/PS_Editor/Content/M_PS_Editor_SplineLine.uasset
Normal file
BIN
Unreal/Plugins/PS_Editor/Content/M_PS_Editor_SplineLine.uasset
Normal file
Binary file not shown.
@@ -13,6 +13,7 @@
|
|||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "PS_Editor_Gizmo.h"
|
#include "PS_Editor_Gizmo.h"
|
||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
|
#include "PS_Editor_SplineActor.h"
|
||||||
#include "DrawDebugHelpers.h"
|
#include "DrawDebugHelpers.h"
|
||||||
#include "PS_Editor_ConfirmDialog.h"
|
#include "PS_Editor_ConfirmDialog.h"
|
||||||
|
|
||||||
@@ -199,6 +200,19 @@ void APS_Editor_Pawn::CreateInputActions()
|
|||||||
IA_Paste = NewObject<UInputAction>(this, TEXT("IA_Paste"));
|
IA_Paste = NewObject<UInputAction>(this, TEXT("IA_Paste"));
|
||||||
IA_Paste->ValueType = EInputActionValueType::Boolean;
|
IA_Paste->ValueType = EInputActionValueType::Boolean;
|
||||||
EditorMappingContext->MapKey(IA_Paste, EKeys::V);
|
EditorMappingContext->MapKey(IA_Paste, EKeys::V);
|
||||||
|
|
||||||
|
// Spline mode / Confirm / Cancel
|
||||||
|
IA_SplineMode = NewObject<UInputAction>(this, TEXT("IA_SplineMode"));
|
||||||
|
IA_SplineMode->ValueType = EInputActionValueType::Boolean;
|
||||||
|
EditorMappingContext->MapKey(IA_SplineMode, EKeys::S);
|
||||||
|
|
||||||
|
IA_Confirm = NewObject<UInputAction>(this, TEXT("IA_Confirm"));
|
||||||
|
IA_Confirm->ValueType = EInputActionValueType::Boolean;
|
||||||
|
EditorMappingContext->MapKey(IA_Confirm, EKeys::Enter);
|
||||||
|
|
||||||
|
IA_Cancel = NewObject<UInputAction>(this, TEXT("IA_Cancel"));
|
||||||
|
IA_Cancel->ValueType = EInputActionValueType::Boolean;
|
||||||
|
EditorMappingContext->MapKey(IA_Cancel, EKeys::Escape);
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
||||||
@@ -242,6 +256,9 @@ void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComp
|
|||||||
EIC->BindAction(IA_Duplicate, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleDuplicate);
|
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_Copy, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleCopy);
|
||||||
EIC->BindAction(IA_Paste, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandlePaste);
|
EIC->BindAction(IA_Paste, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandlePaste);
|
||||||
|
EIC->BindAction(IA_SplineMode, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleSplineMode);
|
||||||
|
EIC->BindAction(IA_Confirm, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleConfirm);
|
||||||
|
EIC->BindAction(IA_Cancel, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleCancel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Input Handlers ----
|
// ---- Input Handlers ----
|
||||||
@@ -357,11 +374,26 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
const FVector ConstrainedDelta = DisplacementAlongAxis * AxisDir;
|
const FVector ConstrainedDelta = DisplacementAlongAxis * AxisDir;
|
||||||
|
|
||||||
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
|
// Spline point mode: move the single point instead of actors
|
||||||
|
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
|
||||||
{
|
{
|
||||||
if (AActor* Actor = Selected[i].Get())
|
if (APS_Editor_SplineActor* SplineActor = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get()))
|
||||||
{
|
{
|
||||||
Actor->SetActorLocation(GizmoDragInitialTransforms[i].GetLocation() + ConstrainedDelta);
|
if (SplinePointDragInitialState.IsValidIndex(ActiveSplinePointIndex))
|
||||||
|
{
|
||||||
|
const FVector NewPos = SplinePointDragInitialState[ActiveSplinePointIndex] + ConstrainedDelta;
|
||||||
|
SplineActor->MovePoint(ActiveSplinePointIndex, NewPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
|
||||||
|
{
|
||||||
|
if (AActor* Actor = Selected[i].Get())
|
||||||
|
{
|
||||||
|
Actor->SetActorLocation(GizmoDragInitialTransforms[i].GetLocation() + ConstrainedDelta);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,10 +464,33 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In translate mode, gizmo follows the selection. In rotate/scale, stay at initial position.
|
// In translate mode, gizmo follows the target
|
||||||
if (Mode == EPS_Editor_TransformMode::Translate)
|
if (Mode == EPS_Editor_TransformMode::Translate)
|
||||||
{
|
{
|
||||||
Gizmo->SetActorLocation(SM->GetSelectionPivot());
|
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
|
||||||
|
{
|
||||||
|
// Following a single spline point
|
||||||
|
if (APS_Editor_SplineActor* SplineActor = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get()))
|
||||||
|
{
|
||||||
|
Gizmo->SetActorLocation(SplineActor->GetPointLocation(ActiveSplinePointIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Selected.Num() == 1)
|
||||||
|
{
|
||||||
|
// Single actor: use centroid for splines, location for others
|
||||||
|
if (APS_Editor_SplineActor* SplineActor = Cast<APS_Editor_SplineActor>(Selected[0].Get()))
|
||||||
|
{
|
||||||
|
Gizmo->SetActorLocation(SplineActor->GetCentroid());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Gizmo->SetActorLocation(SM->GetSelectionPivot());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Gizmo->SetActorLocation(SM->GetSelectionPivot());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update local orientation in real-time during rotation drag
|
// Update local orientation in real-time during rotation drag
|
||||||
@@ -447,6 +502,25 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (CameraMode == EPS_Editor_CameraMode::SplinePointDrag)
|
||||||
|
{
|
||||||
|
// Drag spline point along horizontal plane
|
||||||
|
APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get());
|
||||||
|
if (!Spline || ActiveSplinePointIndex < 0) return;
|
||||||
|
|
||||||
|
FVector RayOrigin, RayDir;
|
||||||
|
if (!GetMouseWorldRay(RayOrigin, RayDir)) return;
|
||||||
|
|
||||||
|
// Ray-plane intersection
|
||||||
|
const float Denom = FVector::DotProduct(RayDir, SplinePointDragPlaneNormal);
|
||||||
|
if (FMath::Abs(Denom) < KINDA_SMALL_NUMBER) return;
|
||||||
|
|
||||||
|
const float T = FVector::DotProduct(SplinePointDragPlaneOrigin - RayOrigin, SplinePointDragPlaneNormal) / Denom;
|
||||||
|
if (T < 0.0f) return;
|
||||||
|
|
||||||
|
const FVector NewPos = RayOrigin + RayDir * T;
|
||||||
|
Spline->MovePoint(ActiveSplinePointIndex, NewPos);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_Pawn::HandleScroll(const FInputActionValue& Value)
|
void APS_Editor_Pawn::HandleScroll(const FInputActionValue& Value)
|
||||||
@@ -502,6 +576,60 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for spline point handle hit → select point, move gizmo to it (no drag yet)
|
||||||
|
if (APS_Editor_PlayerController* EdPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
|
{
|
||||||
|
if (EdPC->GetEditorMode() == EPS_Editor_EditorMode::Normal)
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SelectionManager* SM = EdPC->GetSelectionManager())
|
||||||
|
{
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||||
|
{
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Weak.Get()))
|
||||||
|
{
|
||||||
|
// 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;
|
||||||
|
FCollisionQueryParams SplineParams;
|
||||||
|
SplineParams.AddIgnoredActor(this);
|
||||||
|
const FVector TraceEnd = RayOrigin + RayDir * 100000.0f;
|
||||||
|
if (GetWorld()->LineTraceSingleByChannel(SplineHit, RayOrigin, TraceEnd, ECC_Visibility, SplineParams))
|
||||||
|
{
|
||||||
|
if (Spline->IsCenterCube(SplineHit.GetComponent()))
|
||||||
|
{
|
||||||
|
// Center cube hit → reset point selection, let normal flow handle it
|
||||||
|
ActiveSplinePointIndex = -1;
|
||||||
|
DraggedSplineActor = nullptr;
|
||||||
|
Spline->ClearHandleHighlight();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 HandleIdx = Spline->GetHandleIndexUnderCursor(RayOrigin, RayDir);
|
||||||
|
if (HandleIdx >= 0)
|
||||||
|
{
|
||||||
|
ActiveSplinePointIndex = HandleIdx;
|
||||||
|
DraggedSplineActor = Spline;
|
||||||
|
Spline->HighlightHandle(HandleIdx);
|
||||||
|
|
||||||
|
if (APS_Editor_Gizmo* Gizmo = SM->GetGizmo())
|
||||||
|
{
|
||||||
|
Gizmo->SetActorLocation(Spline->GetPointLocation(HandleIdx));
|
||||||
|
}
|
||||||
|
|
||||||
|
SM->SetTransformMode(EPS_Editor_TransformMode::Translate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clicking elsewhere resets spline point selection
|
||||||
|
ActiveSplinePointIndex = -1;
|
||||||
|
DraggedSplineActor = nullptr;
|
||||||
|
|
||||||
// No gizmo hit: enter PendingClick
|
// No gizmo hit: enter PendingClick
|
||||||
PendingClickScreenPos = GetMouseScreenPosition();
|
PendingClickScreenPos = GetMouseScreenPosition();
|
||||||
SetCameraMode(EPS_Editor_CameraMode::PendingClick);
|
SetCameraMode(EPS_Editor_CameraMode::PendingClick);
|
||||||
@@ -522,8 +650,22 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
|
|||||||
{
|
{
|
||||||
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||||
{
|
{
|
||||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
// Spline point drag: record SplinePointAction
|
||||||
|
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
|
||||||
{
|
{
|
||||||
|
if (APS_Editor_SplineActor* SplineActor = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get()))
|
||||||
|
{
|
||||||
|
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
|
||||||
|
Action->SplineActor = SplineActor;
|
||||||
|
Action->OldPoints = SplinePointDragInitialState;
|
||||||
|
Action->NewPoints = SplineActor->GetAllPointLocations();
|
||||||
|
Action->Description = TEXT("Move spline point");
|
||||||
|
UM->RecordAction(Action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||||
|
{
|
||||||
|
// Normal: record TransformAction
|
||||||
TSharedPtr<FPS_Editor_TransformAction> Action = MakeShared<FPS_Editor_TransformAction>();
|
TSharedPtr<FPS_Editor_TransformAction> Action = MakeShared<FPS_Editor_TransformAction>();
|
||||||
const TArray<TWeakObjectPtr<AActor>>& Selected = SM->GetSelectedActors();
|
const TArray<TWeakObjectPtr<AActor>>& Selected = SM->GetSelectedActors();
|
||||||
|
|
||||||
@@ -555,17 +697,53 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
ActiveGizmoAxis = EPS_Editor_GizmoAxis::None;
|
ActiveGizmoAxis = EPS_Editor_GizmoAxis::None;
|
||||||
|
|
||||||
// Refresh gizmo orientation (local mode needs to track actor's new rotation)
|
// Refresh gizmo: keep at spline point if editing, else refresh orientation
|
||||||
if (APS_Editor_PlayerController* PC2 = Cast<APS_Editor_PlayerController>(GetController()))
|
if (APS_Editor_PlayerController* PC2 = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
{
|
{
|
||||||
if (UPS_Editor_SelectionManager* SM2 = PC2->GetSelectionManager())
|
if (UPS_Editor_SelectionManager* SM2 = PC2->GetSelectionManager())
|
||||||
{
|
{
|
||||||
SM2->RefreshGizmoOrientation();
|
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
|
||||||
|
{
|
||||||
|
// Keep gizmo at the active spline point
|
||||||
|
if (APS_Editor_SplineActor* SplineActor = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get()))
|
||||||
|
{
|
||||||
|
if (APS_Editor_Gizmo* G = SM2->GetGizmo())
|
||||||
|
{
|
||||||
|
G->SetActorLocation(SplineActor->GetPointLocation(ActiveSplinePointIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SM2->RefreshGizmoOrientation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
||||||
}
|
}
|
||||||
|
else if (CameraMode == EPS_Editor_CameraMode::SplinePointDrag)
|
||||||
|
{
|
||||||
|
// Record undo for spline point move
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get()))
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
|
{
|
||||||
|
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||||
|
{
|
||||||
|
TSharedPtr<FPS_Editor_SplinePointAction> Action = MakeShared<FPS_Editor_SplinePointAction>();
|
||||||
|
Action->SplineActor = Spline;
|
||||||
|
Action->OldPoints = SplinePointDragInitialState;
|
||||||
|
Action->NewPoints = Spline->GetAllPointLocations();
|
||||||
|
Action->Description = TEXT("Move spline point");
|
||||||
|
UM->RecordAction(Action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ActiveSplinePointIndex = -1;
|
||||||
|
DraggedSplineActor = nullptr;
|
||||||
|
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
||||||
|
}
|
||||||
else if (CameraMode == EPS_Editor_CameraMode::Pan || CameraMode == EPS_Editor_CameraMode::Orbit)
|
else if (CameraMode == EPS_Editor_CameraMode::Pan || CameraMode == EPS_Editor_CameraMode::Orbit)
|
||||||
{
|
{
|
||||||
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
||||||
@@ -574,6 +752,15 @@ 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->FinishSplinePlacement();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
SetCameraMode(EPS_Editor_CameraMode::Fly);
|
SetCameraMode(EPS_Editor_CameraMode::Fly);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -760,6 +947,44 @@ void APS_Editor_Pawn::HandlePaste(const FInputActionValue& Value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Pawn::HandleSplineMode(const FInputActionValue& Value)
|
||||||
|
{
|
||||||
|
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
|
{
|
||||||
|
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
|
{
|
||||||
|
EditorPC->FinishSplinePlacement();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EditorPC->BeginSplinePlacement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Pawn::HandleConfirm(const FInputActionValue& Value)
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
|
{
|
||||||
|
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
|
{
|
||||||
|
EditorPC->FinishSplinePlacement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Pawn::HandleCancel(const FInputActionValue& Value)
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
|
{
|
||||||
|
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
|
{
|
||||||
|
EditorPC->CancelSplinePlacement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
|
|
||||||
void APS_Editor_Pawn::SetCameraMode(EPS_Editor_CameraMode NewMode)
|
void APS_Editor_Pawn::SetCameraMode(EPS_Editor_CameraMode NewMode)
|
||||||
@@ -944,5 +1169,14 @@ void APS_Editor_Pawn::BeginGizmoDrag(EPS_Editor_GizmoAxis Axis)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store initial spline state if editing a point
|
||||||
|
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
|
||||||
|
{
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(DraggedSplineActor.Get()))
|
||||||
|
{
|
||||||
|
SplinePointDragInitialState = Spline->GetAllPointLocations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SetCameraMode(EPS_Editor_CameraMode::GizmoDrag);
|
SetCameraMode(EPS_Editor_CameraMode::GizmoDrag);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ enum class EPS_Editor_CameraMode : uint8
|
|||||||
/** Alt + left-click or middle-click: orbit around focal point. */
|
/** Alt + left-click or middle-click: orbit around focal point. */
|
||||||
Orbit,
|
Orbit,
|
||||||
/** Dragging a gizmo handle. */
|
/** Dragging a gizmo handle. */
|
||||||
GizmoDrag
|
GizmoDrag,
|
||||||
|
/** Dragging a spline point handle. */
|
||||||
|
SplinePointDrag
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -180,6 +182,22 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<UInputAction> IA_Paste;
|
TObjectPtr<UInputAction> IA_Paste;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UInputAction> IA_SplineMode;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UInputAction> IA_Confirm;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UInputAction> IA_Cancel;
|
||||||
|
|
||||||
|
// ---- Spline point drag state ----
|
||||||
|
int32 ActiveSplinePointIndex = -1;
|
||||||
|
TWeakObjectPtr<AActor> DraggedSplineActor;
|
||||||
|
TArray<FVector> SplinePointDragInitialState;
|
||||||
|
FVector SplinePointDragPlaneOrigin;
|
||||||
|
FVector SplinePointDragPlaneNormal;
|
||||||
|
|
||||||
// ---- Modifier key state ----
|
// ---- Modifier key state ----
|
||||||
bool bAltHeld = false;
|
bool bAltHeld = false;
|
||||||
bool bCtrlHeld = false;
|
bool bCtrlHeld = false;
|
||||||
@@ -216,6 +234,9 @@ private:
|
|||||||
void HandleDuplicate(const FInputActionValue& Value);
|
void HandleDuplicate(const FInputActionValue& Value);
|
||||||
void HandleCopy(const FInputActionValue& Value);
|
void HandleCopy(const FInputActionValue& Value);
|
||||||
void HandlePaste(const FInputActionValue& Value);
|
void HandlePaste(const FInputActionValue& Value);
|
||||||
|
void HandleSplineMode(const FInputActionValue& Value);
|
||||||
|
void HandleConfirm(const FInputActionValue& Value);
|
||||||
|
void HandleCancel(const FInputActionValue& Value);
|
||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
void CreateInputActions();
|
void CreateInputActions();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#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"
|
||||||
|
#include "PS_Editor_SplineActor.h"
|
||||||
|
|
||||||
APS_Editor_PlayerController::APS_Editor_PlayerController()
|
APS_Editor_PlayerController::APS_Editor_PlayerController()
|
||||||
{
|
{
|
||||||
@@ -80,8 +81,116 @@ void APS_Editor_PlayerController::OnPossess(APawn* InPawn)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void APS_Editor_PlayerController::BeginSplinePlacement()
|
||||||
|
{
|
||||||
|
if (SelectionManager)
|
||||||
|
{
|
||||||
|
SelectionManager->ClearSelection();
|
||||||
|
}
|
||||||
|
ActiveSplineActor = nullptr;
|
||||||
|
EditorMode = EPS_Editor_EditorMode::SplinePlacement;
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline placement mode started"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_PlayerController::FinishSplinePlacement()
|
||||||
|
{
|
||||||
|
if (!ActiveSplineActor || ActiveSplineActor->GetNumPoints() < 2)
|
||||||
|
{
|
||||||
|
CancelSplinePlacement();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide handles after placement (shown again on selection)
|
||||||
|
ActiveSplineActor->SetHandlesVisible(false);
|
||||||
|
|
||||||
|
// Track in SpawnManager — CRITICAL for outliner, save/load, and scene clear
|
||||||
|
if (SpawnManager)
|
||||||
|
{
|
||||||
|
SpawnManager->TrackSpawnedActor(ActiveSplineActor);
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline tracked by SpawnManager (total: %d)"), SpawnManager->GetSpawnedActors().Num());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Error, TEXT("PS_Editor: SpawnManager is null! Spline will NOT be tracked."));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record spawn action for undo
|
||||||
|
if (UndoManager)
|
||||||
|
{
|
||||||
|
TSharedPtr<FPS_Editor_SpawnAction> Action = MakeShared<FPS_Editor_SpawnAction>();
|
||||||
|
Action->Actors.Add(ActiveSplineActor);
|
||||||
|
Action->Description = FString::Printf(TEXT("Create spline (%d points)"), ActiveSplineActor->GetNumPoints());
|
||||||
|
UndoManager->RecordAction(Action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select the new spline
|
||||||
|
if (SelectionManager)
|
||||||
|
{
|
||||||
|
SelectionManager->SelectActor(ActiveSplineActor);
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline placed with %d points, class=%s"),
|
||||||
|
ActiveSplineActor->GetNumPoints(), *ActiveSplineActor->GetClass()->GetPathName());
|
||||||
|
|
||||||
|
ActiveSplineActor = nullptr;
|
||||||
|
EditorMode = EPS_Editor_EditorMode::Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_PlayerController::CancelSplinePlacement()
|
||||||
|
{
|
||||||
|
if (ActiveSplineActor)
|
||||||
|
{
|
||||||
|
ActiveSplineActor->Destroy();
|
||||||
|
ActiveSplineActor = nullptr;
|
||||||
|
}
|
||||||
|
EditorMode = EPS_Editor_EditorMode::Normal;
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline placement cancelled"));
|
||||||
|
}
|
||||||
|
|
||||||
void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bAdditive)
|
void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bAdditive)
|
||||||
{
|
{
|
||||||
|
if (EditorMode == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
|
{
|
||||||
|
// Deproject click to world position
|
||||||
|
FVector WorldOrigin, WorldDirection;
|
||||||
|
if (!DeprojectScreenPositionToWorld(ScreenPos.X, ScreenPos.Y, WorldOrigin, WorldDirection))
|
||||||
|
return;
|
||||||
|
|
||||||
|
FHitResult Hit;
|
||||||
|
FCollisionQueryParams Params;
|
||||||
|
Params.AddIgnoredActor(GetPawn());
|
||||||
|
if (ActiveSplineActor) Params.AddIgnoredActor(ActiveSplineActor);
|
||||||
|
|
||||||
|
const FVector End = WorldOrigin + WorldDirection * 100000.0f;
|
||||||
|
FVector ClickWorldPos;
|
||||||
|
if (GetWorld()->LineTraceSingleByChannel(Hit, WorldOrigin, End, ECC_Visibility, Params))
|
||||||
|
{
|
||||||
|
ClickWorldPos = Hit.ImpactPoint;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ClickWorldPos = WorldOrigin + WorldDirection * 1000.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ActiveSplineActor)
|
||||||
|
{
|
||||||
|
// First click: spawn the spline actor
|
||||||
|
FActorSpawnParameters SpawnParams;
|
||||||
|
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||||
|
ActiveSplineActor = GetWorld()->SpawnActor<APS_Editor_SplineActor>(
|
||||||
|
APS_Editor_SplineActor::StaticClass(), ClickWorldPos, FRotator::ZeroRotator, SpawnParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ActiveSplineActor)
|
||||||
|
{
|
||||||
|
ActiveSplineActor->AddPoint(ClickWorldPos);
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline point %d added at %s"),
|
||||||
|
ActiveSplineActor->GetNumPoints(), *ClickWorldPos.ToString());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal mode: selection
|
||||||
if (SelectionManager)
|
if (SelectionManager)
|
||||||
{
|
{
|
||||||
SelectionManager->HandleClickAtScreenPosition(ScreenPos, bAdditive);
|
SelectionManager->HandleClickAtScreenPosition(ScreenPos, bAdditive);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
|
#include "PS_Editor_SelectionTypes.h"
|
||||||
#include "PS_Editor_PlayerController.generated.h"
|
#include "PS_Editor_PlayerController.generated.h"
|
||||||
|
|
||||||
class UPS_Editor_SelectionManager;
|
class UPS_Editor_SelectionManager;
|
||||||
@@ -9,6 +10,7 @@ class UPS_Editor_UndoManager;
|
|||||||
class UPS_Editor_SpawnManager;
|
class UPS_Editor_SpawnManager;
|
||||||
class UPS_Editor_SpawnCatalog;
|
class UPS_Editor_SpawnCatalog;
|
||||||
class UPS_Editor_SceneSerializer;
|
class UPS_Editor_SceneSerializer;
|
||||||
|
class APS_Editor_SplineActor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Player controller for PS_Editor runtime editing.
|
* Player controller for PS_Editor runtime editing.
|
||||||
@@ -38,6 +40,23 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS Editor")
|
||||||
TSoftObjectPtr<UPS_Editor_SpawnCatalog> SpawnCatalogAsset;
|
TSoftObjectPtr<UPS_Editor_SpawnCatalog> SpawnCatalogAsset;
|
||||||
|
|
||||||
|
// ---- Editor Mode (Normal / SplinePlacement) ----
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||||
|
EPS_Editor_EditorMode GetEditorMode() const { return EditorMode; }
|
||||||
|
|
||||||
|
/** Enter spline placement mode. Clicks will add points. */
|
||||||
|
void BeginSplinePlacement();
|
||||||
|
|
||||||
|
/** Finish current spline (min 2 points required). Returns to Normal mode. */
|
||||||
|
void FinishSplinePlacement();
|
||||||
|
|
||||||
|
/** Cancel current spline placement (destroys in-progress spline). */
|
||||||
|
void CancelSplinePlacement();
|
||||||
|
|
||||||
|
/** Get the spline currently being placed (nullptr if not in SplinePlacement mode). */
|
||||||
|
APS_Editor_SplineActor* GetActiveSplineActor() const { return ActiveSplineActor; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
virtual void OnPossess(APawn* InPawn) override;
|
virtual void OnPossess(APawn* InPawn) override;
|
||||||
@@ -55,5 +74,10 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<UPS_Editor_SceneSerializer> SceneSerializer;
|
TObjectPtr<UPS_Editor_SceneSerializer> SceneSerializer;
|
||||||
|
|
||||||
|
EPS_Editor_EditorMode EditorMode = EPS_Editor_EditorMode::Normal;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<APS_Editor_SplineActor> ActiveSplineActor;
|
||||||
|
|
||||||
void HandleEditorClick(FVector2D ScreenPos, bool bAdditive);
|
void HandleEditorClick(FVector2D ScreenPos, bool bAdditive);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public class PS_Editor : ModuleRules
|
|||||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "Gizmo"));
|
PublicIncludePaths.Add(Path.Combine(SourceDir, "Gizmo"));
|
||||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "Spawn"));
|
PublicIncludePaths.Add(Path.Combine(SourceDir, "Spawn"));
|
||||||
PublicIncludePaths.Add(Path.Combine(SourceDir, "Serialization"));
|
PublicIncludePaths.Add(Path.Combine(SourceDir, "Serialization"));
|
||||||
|
PublicIncludePaths.Add(Path.Combine(SourceDir, "Spline"));
|
||||||
|
|
||||||
PublicDependencyModuleNames.AddRange(new string[]
|
PublicDependencyModuleNames.AddRange(new string[]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include "Materials/MaterialExpressionLinearInterpolate.h"
|
#include "Materials/MaterialExpressionLinearInterpolate.h"
|
||||||
#include "Materials/MaterialExpressionComponentMask.h"
|
#include "Materials/MaterialExpressionComponentMask.h"
|
||||||
#include "Materials/MaterialExpressionAppendVector.h"
|
#include "Materials/MaterialExpressionAppendVector.h"
|
||||||
|
#include "Materials/MaterialExpressionVectorParameter.h"
|
||||||
#include "AssetRegistry/AssetRegistryModule.h"
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
#include "UObject/SavePackage.h"
|
#include "UObject/SavePackage.h"
|
||||||
#include "Misc/PackageName.h"
|
#include "Misc/PackageName.h"
|
||||||
@@ -218,6 +219,48 @@ static void CreateOutlineMaterialAsset()
|
|||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selection outline material auto-generated (8-sample, dynamic resolution)"));
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selection outline material auto-generated (8-sample, dynamic resolution)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void CreateSplineLineMaterialAsset()
|
||||||
|
{
|
||||||
|
const FString MaterialPath = TEXT("/PS_Editor/M_PS_Editor_SplineLine");
|
||||||
|
const FString AssetName = TEXT("M_PS_Editor_SplineLine");
|
||||||
|
|
||||||
|
if (LoadObject<UMaterial>(nullptr, *(MaterialPath + TEXT(".") + AssetName)))
|
||||||
|
{
|
||||||
|
return; // Already exists
|
||||||
|
}
|
||||||
|
|
||||||
|
UPackage* Package = CreatePackage(*MaterialPath);
|
||||||
|
UMaterial* Mat = NewObject<UMaterial>(Package, *AssetName, RF_Public | RF_Standalone);
|
||||||
|
Mat->MaterialDomain = EMaterialDomain::MD_Surface;
|
||||||
|
Mat->SetShadingModel(EMaterialShadingModel::MSM_Unlit);
|
||||||
|
Mat->BlendMode = EBlendMode::BLEND_Opaque;
|
||||||
|
Mat->TwoSided = true;
|
||||||
|
|
||||||
|
auto AddExpr = [&](UMaterialExpression* Expr) { Mat->GetExpressionCollection().AddExpression(Expr); };
|
||||||
|
|
||||||
|
// Vector parameter "Color" (default orange)
|
||||||
|
UMaterialExpressionVectorParameter* ColorParam = NewObject<UMaterialExpressionVectorParameter>(Mat);
|
||||||
|
ColorParam->ParameterName = TEXT("Color");
|
||||||
|
ColorParam->DefaultValue = FLinearColor(1.0f, 0.4f, 0.0f, 1.0f);
|
||||||
|
AddExpr(ColorParam);
|
||||||
|
ColorParam->MaterialExpressionEditorX = -200;
|
||||||
|
ColorParam->MaterialExpressionEditorY = 0;
|
||||||
|
|
||||||
|
// Connect Color → Emissive
|
||||||
|
Mat->GetEditorOnlyData()->EmissiveColor.Connect(0, ColorParam);
|
||||||
|
|
||||||
|
Mat->PreEditChange(nullptr);
|
||||||
|
Mat->PostEditChange();
|
||||||
|
|
||||||
|
FString PackageFilename = FPackageName::LongPackageNameToFilename(MaterialPath, FPackageName::GetAssetPackageExtension());
|
||||||
|
FSavePackageArgs SaveArgs;
|
||||||
|
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
|
||||||
|
UPackage::SavePackage(Package, Mat, *PackageFilename, SaveArgs);
|
||||||
|
FAssetRegistryModule::AssetCreated(Mat);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spline line material auto-generated (unlit opaque with Color parameter)"));
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void FPS_EditorModule::StartupModule()
|
void FPS_EditorModule::StartupModule()
|
||||||
@@ -226,6 +269,7 @@ void FPS_EditorModule::StartupModule()
|
|||||||
|
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
FCoreDelegates::OnPostEngineInit.AddStatic(&CreateOutlineMaterialAsset);
|
FCoreDelegates::OnPostEngineInit.AddStatic(&CreateOutlineMaterialAsset);
|
||||||
|
FCoreDelegates::OnPostEngineInit.AddStatic(&CreateSplineLineMaterialAsset);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "PS_Editor_SpawnManager.h"
|
#include "PS_Editor_SpawnManager.h"
|
||||||
#include "PS_Editor_EditableComponent.h"
|
#include "PS_Editor_EditableComponent.h"
|
||||||
|
#include "PS_Editor_SplineActor.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"
|
||||||
@@ -185,6 +186,13 @@ void UPS_Editor_SelectionManager::SelectActor(AActor* Actor)
|
|||||||
UpdateGizmo();
|
UpdateGizmo();
|
||||||
OnSelectionChanged.Broadcast();
|
OnSelectionChanged.Broadcast();
|
||||||
|
|
||||||
|
// Show spline handles and mark selected
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Actor))
|
||||||
|
{
|
||||||
|
Spline->SetHandlesVisible(true);
|
||||||
|
Spline->SetSelected(true);
|
||||||
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selected %s"), *Actor->GetName());
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selected %s"), *Actor->GetName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,6 +214,13 @@ void UPS_Editor_SelectionManager::DeselectActor(AActor* Actor)
|
|||||||
UpdateGizmo();
|
UpdateGizmo();
|
||||||
OnSelectionChanged.Broadcast();
|
OnSelectionChanged.Broadcast();
|
||||||
|
|
||||||
|
// Hide spline handles and unmark
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Actor))
|
||||||
|
{
|
||||||
|
Spline->SetHandlesVisible(false);
|
||||||
|
Spline->SetSelected(false);
|
||||||
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Deselected %s"), *Actor->GetName());
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Deselected %s"), *Actor->GetName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,7 +284,15 @@ FVector UPS_Editor_SelectionManager::GetSelectionPivot() const
|
|||||||
{
|
{
|
||||||
if (AActor* Actor = Weak.Get())
|
if (AActor* Actor = Weak.Get())
|
||||||
{
|
{
|
||||||
Sum += Actor->GetActorLocation();
|
// For SplineActors, use the centroid of all points as pivot
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Actor))
|
||||||
|
{
|
||||||
|
Sum += Spline->GetCentroid();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Sum += Actor->GetActorLocation();
|
||||||
|
}
|
||||||
Count++;
|
Count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -448,7 +471,18 @@ void UPS_Editor_SelectionManager::UpdateGizmo()
|
|||||||
|
|
||||||
if (IsAnythingSelected())
|
if (IsAnythingSelected())
|
||||||
{
|
{
|
||||||
Gizmo->SetGizmoActive(true, GetSelectionPivot());
|
FVector Pivot = GetSelectionPivot();
|
||||||
|
|
||||||
|
// Override pivot for SplineActors: use centroid so gizmo aligns with center cube
|
||||||
|
if (SelectedActors.Num() == 1)
|
||||||
|
{
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(SelectedActors[0].Get()))
|
||||||
|
{
|
||||||
|
Pivot = Spline->GetCentroid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Gizmo->SetGizmoActive(true, Pivot);
|
||||||
|
|
||||||
// Update gizmo orientation
|
// Update gizmo orientation
|
||||||
if (AActor* Actor = SelectedActors[0].Get())
|
if (AActor* Actor = SelectedActors[0].Get())
|
||||||
|
|||||||
@@ -33,3 +33,11 @@ enum class EPS_Editor_Space : uint8
|
|||||||
World,
|
World,
|
||||||
Local
|
Local
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Top-level editor mode (separate from transform mode). */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EPS_Editor_EditorMode : uint8
|
||||||
|
{
|
||||||
|
Normal, // Standard select/transform behavior
|
||||||
|
SplinePlacement // Clicks add points to current spline
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,24 @@
|
|||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
|
#include "PS_Editor_SplineActor.h"
|
||||||
#include "UObject/UnrealType.h"
|
#include "UObject/UnrealType.h"
|
||||||
#include "Components/ActorComponent.h"
|
#include "Components/ActorComponent.h"
|
||||||
|
|
||||||
|
void FPS_Editor_SplinePointAction::Execute()
|
||||||
|
{
|
||||||
|
if (APS_Editor_SplineActor* Spline = SplineActor.Get())
|
||||||
|
{
|
||||||
|
Spline->SetAllPointLocations(NewPoints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FPS_Editor_SplinePointAction::Undo()
|
||||||
|
{
|
||||||
|
if (APS_Editor_SplineActor* Spline = SplineActor.Get())
|
||||||
|
{
|
||||||
|
Spline->SetAllPointLocations(OldPoints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void FPS_Editor_PropertyAction::ApplyValue(const FString& ValueStr)
|
void FPS_Editor_PropertyAction::ApplyValue(const FString& ValueStr)
|
||||||
{
|
{
|
||||||
AActor* Act = Actor.Get();
|
AActor* Act = Actor.Get();
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ enum class EPS_Editor_ActionType : uint8
|
|||||||
Transform,
|
Transform,
|
||||||
Delete,
|
Delete,
|
||||||
Spawn,
|
Spawn,
|
||||||
PropertyChange
|
PropertyChange,
|
||||||
|
SplinePointChange
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -157,6 +158,26 @@ struct FPS_Editor_SpawnAction : public FPS_Editor_Action
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class APS_Editor_SplineActor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a spline point state change (move, add, remove).
|
||||||
|
* Stores full state snapshots (all points before/after) for simplicity.
|
||||||
|
*/
|
||||||
|
struct FPS_Editor_SplinePointAction : public FPS_Editor_Action
|
||||||
|
{
|
||||||
|
FPS_Editor_SplinePointAction() : FPS_Editor_Action(EPS_Editor_ActionType::SplinePointChange) {}
|
||||||
|
|
||||||
|
TWeakObjectPtr<APS_Editor_SplineActor> SplineActor;
|
||||||
|
TArray<FVector> OldPoints;
|
||||||
|
TArray<FVector> NewPoints;
|
||||||
|
FString Description;
|
||||||
|
|
||||||
|
virtual void Execute() override;
|
||||||
|
virtual void Undo() override;
|
||||||
|
virtual FString GetDescription() const override { return Description; }
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Records a single property value change on a component or actor.
|
* Records a single property value change on a component or actor.
|
||||||
* Stores text-serialized old/new values for undo/redo via ImportText.
|
* Stores text-serialized old/new values for undo/redo via ImportText.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "PS_Editor_SpawnManager.h"
|
#include "PS_Editor_SpawnManager.h"
|
||||||
#include "PS_Editor_SelectionManager.h"
|
#include "PS_Editor_SelectionManager.h"
|
||||||
#include "PS_Editor_EditableComponent.h"
|
#include "PS_Editor_EditableComponent.h"
|
||||||
|
#include "PS_Editor_SplineActor.h"
|
||||||
#include "JsonObjectConverter.h"
|
#include "JsonObjectConverter.h"
|
||||||
#include "Misc/FileHelper.h"
|
#include "Misc/FileHelper.h"
|
||||||
#include "Misc/Paths.h"
|
#include "Misc/Paths.h"
|
||||||
@@ -36,6 +37,14 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
|
|||||||
ActorData.Rotation = Actor->GetActorRotation();
|
ActorData.Rotation = Actor->GetActorRotation();
|
||||||
ActorData.Scale = Actor->GetActorScale3D();
|
ActorData.Scale = Actor->GetActorScale3D();
|
||||||
|
|
||||||
|
// Export spline data
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(Actor))
|
||||||
|
{
|
||||||
|
Spline->ExportToCustomProperties(ActorData.CustomProperties);
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Exported spline with %d points, class=%s"),
|
||||||
|
Spline->GetNumPoints(), *ActorData.ClassPath);
|
||||||
|
}
|
||||||
|
|
||||||
// Export custom editable properties
|
// Export custom editable properties
|
||||||
if (UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>())
|
if (UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>())
|
||||||
{
|
{
|
||||||
@@ -137,6 +146,11 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_
|
|||||||
{
|
{
|
||||||
UClass* LoadedClass = LoadObject<UClass>(nullptr, *ActorData.ClassPath);
|
UClass* LoadedClass = LoadObject<UClass>(nullptr, *ActorData.ClassPath);
|
||||||
if (!LoadedClass)
|
if (!LoadedClass)
|
||||||
|
{
|
||||||
|
// Fallback for C++ native classes (e.g. /Script/PS_Editor.PS_Editor_SplineActor)
|
||||||
|
LoadedClass = FindObject<UClass>(nullptr, *ActorData.ClassPath);
|
||||||
|
}
|
||||||
|
if (!LoadedClass)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Failed to load class: %s"), *ActorData.ClassPath);
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Failed to load class: %s"), *ActorData.ClassPath);
|
||||||
continue;
|
continue;
|
||||||
@@ -190,6 +204,12 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Import spline data
|
||||||
|
if (APS_Editor_SplineActor* Spline = Cast<APS_Editor_SplineActor>(SpawnedActor))
|
||||||
|
{
|
||||||
|
Spline->ImportFromCustomProperties(ActorData.CustomProperties);
|
||||||
|
}
|
||||||
|
|
||||||
SpawnedCount++;
|
SpawnedCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,493 @@
|
|||||||
|
#include "PS_Editor_SplineActor.h"
|
||||||
|
#include "Components/SplineComponent.h"
|
||||||
|
#include "ProceduralMeshComponent.h"
|
||||||
|
#include "Components/StaticMeshComponent.h"
|
||||||
|
#include "Engine/StaticMesh.h"
|
||||||
|
#include "Materials/MaterialInstanceDynamic.h"
|
||||||
|
|
||||||
|
APS_Editor_SplineActor::APS_Editor_SplineActor()
|
||||||
|
{
|
||||||
|
PrimaryActorTick.bCanEverTick = false;
|
||||||
|
|
||||||
|
// Root
|
||||||
|
USceneComponent* Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
|
||||||
|
SetRootComponent(Root);
|
||||||
|
Root->SetMobility(EComponentMobility::Movable);
|
||||||
|
|
||||||
|
// Spline data
|
||||||
|
SplineComp = CreateDefaultSubobject<USplineComponent>(TEXT("SplineComp"));
|
||||||
|
SplineComp->SetupAttachment(Root);
|
||||||
|
SplineComp->ClearSplinePoints(false);
|
||||||
|
SplineComp->SetClosedLoop(false);
|
||||||
|
|
||||||
|
// Line mesh
|
||||||
|
SplineMeshComp = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("SplineMesh"));
|
||||||
|
SplineMeshComp->SetupAttachment(Root);
|
||||||
|
SplineMeshComp->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||||
|
SplineMeshComp->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||||
|
SplineMeshComp->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
|
||||||
|
SplineMeshComp->bUseComplexAsSimpleCollision = true;
|
||||||
|
SplineMeshComp->SetCastShadow(false);
|
||||||
|
|
||||||
|
// Handle parent
|
||||||
|
HandleRoot = CreateDefaultSubobject<USceneComponent>(TEXT("HandleRoot"));
|
||||||
|
HandleRoot->SetupAttachment(Root);
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::BeginPlay()
|
||||||
|
{
|
||||||
|
Super::BeginPlay();
|
||||||
|
|
||||||
|
// Gizmo material (no depth test, renders on top) for handles and selected state
|
||||||
|
UMaterialInterface* GizmoMat = LoadObject<UMaterialInterface>(
|
||||||
|
nullptr, TEXT("/PS_Editor/M_PS_Editor_Gizmo.M_PS_Editor_Gizmo"));
|
||||||
|
|
||||||
|
// Auto-generated opaque unlit material (depth tested) for spline line
|
||||||
|
UMaterialInterface* SplineLineMat = LoadObject<UMaterialInterface>(
|
||||||
|
nullptr, TEXT("/PS_Editor/M_PS_Editor_SplineLine.M_PS_Editor_SplineLine"));
|
||||||
|
|
||||||
|
// Spline line: normal depth-tested material (orange)
|
||||||
|
if (SplineLineMat)
|
||||||
|
{
|
||||||
|
SplineMat = UMaterialInstanceDynamic::Create(SplineLineMat, this);
|
||||||
|
SplineMat->SetVectorParameterValue(TEXT("Color"), FLinearColor(1.0f, 0.4f, 0.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spline line selected: no depth test (on top, bright orange)
|
||||||
|
if (GizmoMat)
|
||||||
|
{
|
||||||
|
SplineMatSelected = UMaterialInstanceDynamic::Create(GizmoMat, this);
|
||||||
|
SplineMatSelected->SetVectorParameterValue(TEXT("Color"), FLinearColor(1.0f, 0.6f, 0.0f));
|
||||||
|
|
||||||
|
// Handles always use gizmo material (on top, clickable)
|
||||||
|
HandleMat = UMaterialInstanceDynamic::Create(GizmoMat, this);
|
||||||
|
HandleMat->SetVectorParameterValue(TEXT("Color"), FLinearColor(1.0f, 1.0f, 1.0f)); // White
|
||||||
|
|
||||||
|
HandleHighlightMat = UMaterialInstanceDynamic::Create(GizmoMat, this);
|
||||||
|
HandleHighlightMat->SetVectorParameterValue(TEXT("Color"), FLinearColor(1.0f, 1.0f, 0.0f)); // Yellow
|
||||||
|
|
||||||
|
CenterCubeMat = UMaterialInstanceDynamic::Create(GizmoMat, this);
|
||||||
|
CenterCubeMat->SetVectorParameterValue(TEXT("Color"), FLinearColor(1.0f, 0.4f, 0.0f)); // Orange
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create center cube (selection handle for whole spline)
|
||||||
|
CenterCube = NewObject<UStaticMeshComponent>(this);
|
||||||
|
CenterCube->SetupAttachment(GetRootComponent());
|
||||||
|
CenterCube->RegisterComponent();
|
||||||
|
UStaticMesh* CubeMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
|
||||||
|
if (CubeMesh)
|
||||||
|
{
|
||||||
|
CenterCube->SetStaticMesh(CubeMesh);
|
||||||
|
}
|
||||||
|
CenterCube->SetWorldScale3D(FVector(CenterCubeScale));
|
||||||
|
CenterCube->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||||
|
CenterCube->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||||
|
CenterCube->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
|
||||||
|
CenterCube->SetCastShadow(false);
|
||||||
|
if (CenterCubeMat)
|
||||||
|
{
|
||||||
|
CenterCube->SetMaterial(0, CenterCubeMat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Point manipulation ----
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::AddPoint(const FVector& WorldPosition)
|
||||||
|
{
|
||||||
|
// Convert world to local space
|
||||||
|
const FVector LocalPos = GetActorTransform().InverseTransformPosition(WorldPosition);
|
||||||
|
SplineComp->AddSplinePoint(LocalPos, ESplineCoordinateSpace::Local, true);
|
||||||
|
|
||||||
|
// Create handle
|
||||||
|
UStaticMeshComponent* Handle = CreatePointHandle(WorldPosition);
|
||||||
|
PointHandles.Add(Handle);
|
||||||
|
|
||||||
|
UpdateVisualization();
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::MovePoint(int32 Index, const FVector& NewWorldPosition)
|
||||||
|
{
|
||||||
|
if (Index < 0 || Index >= GetNumPoints()) return;
|
||||||
|
|
||||||
|
const FVector LocalPos = GetActorTransform().InverseTransformPosition(NewWorldPosition);
|
||||||
|
SplineComp->SetLocationAtSplinePoint(Index, LocalPos, ESplineCoordinateSpace::Local, true);
|
||||||
|
|
||||||
|
if (PointHandles.IsValidIndex(Index) && PointHandles[Index])
|
||||||
|
{
|
||||||
|
PointHandles[Index]->SetWorldLocation(NewWorldPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateVisualization();
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::RemovePoint(int32 Index)
|
||||||
|
{
|
||||||
|
if (Index < 0 || Index >= GetNumPoints()) return;
|
||||||
|
|
||||||
|
SplineComp->RemoveSplinePoint(Index, true);
|
||||||
|
|
||||||
|
if (PointHandles.IsValidIndex(Index))
|
||||||
|
{
|
||||||
|
if (PointHandles[Index])
|
||||||
|
{
|
||||||
|
PointHandles[Index]->DestroyComponent();
|
||||||
|
}
|
||||||
|
PointHandles.RemoveAt(Index);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateVisualization();
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 APS_Editor_SplineActor::GetNumPoints() const
|
||||||
|
{
|
||||||
|
return SplineComp ? SplineComp->GetNumberOfSplinePoints() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector APS_Editor_SplineActor::GetPointLocation(int32 Index) const
|
||||||
|
{
|
||||||
|
if (!SplineComp || Index < 0 || Index >= GetNumPoints()) return FVector::ZeroVector;
|
||||||
|
return SplineComp->GetLocationAtSplinePoint(Index, ESplineCoordinateSpace::World);
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FVector> APS_Editor_SplineActor::GetAllPointLocations() const
|
||||||
|
{
|
||||||
|
TArray<FVector> Result;
|
||||||
|
const int32 Num = GetNumPoints();
|
||||||
|
Result.Reserve(Num);
|
||||||
|
for (int32 i = 0; i < Num; i++)
|
||||||
|
{
|
||||||
|
Result.Add(GetPointLocation(i));
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::SetAllPointLocations(const TArray<FVector>& Points)
|
||||||
|
{
|
||||||
|
if (!SplineComp) return;
|
||||||
|
|
||||||
|
SplineComp->ClearSplinePoints(false);
|
||||||
|
for (const FVector& WorldPos : Points)
|
||||||
|
{
|
||||||
|
const FVector LocalPos = GetActorTransform().InverseTransformPosition(WorldPos);
|
||||||
|
SplineComp->AddSplinePoint(LocalPos, ESplineCoordinateSpace::Local, false);
|
||||||
|
}
|
||||||
|
SplineComp->UpdateSpline();
|
||||||
|
|
||||||
|
RebuildHandles();
|
||||||
|
UpdateVisualization();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Visualization ----
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::UpdateVisualization()
|
||||||
|
{
|
||||||
|
if (!SplineMeshComp || !SplineComp || GetNumPoints() < 2)
|
||||||
|
{
|
||||||
|
if (SplineMeshComp) SplineMeshComp->ClearAllMeshSections();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float SplineLength = SplineComp->GetSplineLength();
|
||||||
|
const int32 NumSamples = FMath::Max(2, FMath::CeilToInt(SplineLength / SampleInterval));
|
||||||
|
|
||||||
|
TArray<FVector> Vertices;
|
||||||
|
TArray<int32> Triangles;
|
||||||
|
TArray<FVector> Normals;
|
||||||
|
TArray<FVector2D> UVs;
|
||||||
|
|
||||||
|
// 4 vertices per sample: 2 for vertical ribbon + 2 for horizontal ribbon (cross-section)
|
||||||
|
Vertices.Reserve(NumSamples * 4);
|
||||||
|
Normals.Reserve(NumSamples * 4);
|
||||||
|
UVs.Reserve(NumSamples * 4);
|
||||||
|
|
||||||
|
for (int32 i = 0; i < NumSamples; i++)
|
||||||
|
{
|
||||||
|
const float Distance = (static_cast<float>(i) / (NumSamples - 1)) * SplineLength;
|
||||||
|
const FVector Location = SplineComp->GetLocationAtDistanceAlongSpline(Distance, ESplineCoordinateSpace::Local);
|
||||||
|
const FVector Right = SplineComp->GetRightVectorAtDistanceAlongSpline(Distance, ESplineCoordinateSpace::Local);
|
||||||
|
const FVector Up = FVector(0.0f, 0.0f, LineHalfWidth);
|
||||||
|
|
||||||
|
const float U = static_cast<float>(i) / (NumSamples - 1);
|
||||||
|
|
||||||
|
// Vertical ribbon (visible from sides)
|
||||||
|
Vertices.Add(Location - Up);
|
||||||
|
Vertices.Add(Location + Up);
|
||||||
|
Normals.Add(Right);
|
||||||
|
Normals.Add(Right);
|
||||||
|
UVs.Add(FVector2D(U, 0.0f));
|
||||||
|
UVs.Add(FVector2D(U, 1.0f));
|
||||||
|
|
||||||
|
// Horizontal ribbon (visible from above/below)
|
||||||
|
Vertices.Add(Location - Right * LineHalfWidth);
|
||||||
|
Vertices.Add(Location + Right * LineHalfWidth);
|
||||||
|
Normals.Add(FVector::UpVector);
|
||||||
|
Normals.Add(FVector::UpVector);
|
||||||
|
UVs.Add(FVector2D(U, 0.0f));
|
||||||
|
UVs.Add(FVector2D(U, 1.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate triangles for both ribbons (double-sided)
|
||||||
|
Triangles.Reserve((NumSamples - 1) * 24);
|
||||||
|
for (int32 i = 0; i < NumSamples - 1; i++)
|
||||||
|
{
|
||||||
|
// Vertical ribbon indices
|
||||||
|
const int32 V0 = i * 4;
|
||||||
|
const int32 V1 = i * 4 + 1;
|
||||||
|
const int32 V2 = (i + 1) * 4;
|
||||||
|
const int32 V3 = (i + 1) * 4 + 1;
|
||||||
|
|
||||||
|
// Horizontal ribbon indices
|
||||||
|
const int32 H0 = i * 4 + 2;
|
||||||
|
const int32 H1 = i * 4 + 3;
|
||||||
|
const int32 H2 = (i + 1) * 4 + 2;
|
||||||
|
const int32 H3 = (i + 1) * 4 + 3;
|
||||||
|
|
||||||
|
// Vertical: front + back
|
||||||
|
Triangles.Add(V0); Triangles.Add(V2); Triangles.Add(V1);
|
||||||
|
Triangles.Add(V1); Triangles.Add(V2); Triangles.Add(V3);
|
||||||
|
Triangles.Add(V0); Triangles.Add(V1); Triangles.Add(V2);
|
||||||
|
Triangles.Add(V1); Triangles.Add(V3); Triangles.Add(V2);
|
||||||
|
|
||||||
|
// Horizontal: front + back
|
||||||
|
Triangles.Add(H0); Triangles.Add(H2); Triangles.Add(H1);
|
||||||
|
Triangles.Add(H1); Triangles.Add(H2); Triangles.Add(H3);
|
||||||
|
Triangles.Add(H0); Triangles.Add(H1); Triangles.Add(H2);
|
||||||
|
Triangles.Add(H1); Triangles.Add(H3); Triangles.Add(H2);
|
||||||
|
}
|
||||||
|
|
||||||
|
SplineMeshComp->ClearAllMeshSections();
|
||||||
|
SplineMeshComp->CreateMeshSection(0, Vertices, Triangles, Normals, UVs, TArray<FColor>(), TArray<FProcMeshTangent>(), false);
|
||||||
|
|
||||||
|
// Apply the current material (normal or selected)
|
||||||
|
// Check if handles are visible as a proxy for "selected" state
|
||||||
|
const bool bCurrentlySelected = (PointHandles.Num() > 0 && PointHandles[0] && PointHandles[0]->IsVisible());
|
||||||
|
UMaterialInstanceDynamic* LineMat = (bCurrentlySelected && SplineMatSelected) ? SplineMatSelected : SplineMat;
|
||||||
|
if (LineMat)
|
||||||
|
{
|
||||||
|
SplineMeshComp->SetMaterial(0, LineMat);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateCenterCube();
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::UpdateCenterCube()
|
||||||
|
{
|
||||||
|
if (!CenterCube) return;
|
||||||
|
|
||||||
|
const int32 Num = GetNumPoints();
|
||||||
|
if (Num == 0)
|
||||||
|
{
|
||||||
|
CenterCube->SetVisibility(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position at centroid of all points
|
||||||
|
FVector Centroid = FVector::ZeroVector;
|
||||||
|
for (int32 i = 0; i < Num; i++)
|
||||||
|
{
|
||||||
|
Centroid += GetPointLocation(i);
|
||||||
|
}
|
||||||
|
Centroid /= Num;
|
||||||
|
CenterCube->SetWorldLocation(Centroid);
|
||||||
|
CenterCube->SetVisibility(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Handle interaction ----
|
||||||
|
|
||||||
|
UStaticMeshComponent* APS_Editor_SplineActor::CreatePointHandle(const FVector& WorldPosition)
|
||||||
|
{
|
||||||
|
UStaticMeshComponent* Handle = NewObject<UStaticMeshComponent>(this);
|
||||||
|
Handle->SetupAttachment(HandleRoot);
|
||||||
|
Handle->RegisterComponent();
|
||||||
|
|
||||||
|
// Load sphere mesh
|
||||||
|
UStaticMesh* SphereMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Sphere.Sphere"));
|
||||||
|
if (SphereMesh)
|
||||||
|
{
|
||||||
|
Handle->SetStaticMesh(SphereMesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
Handle->SetWorldLocation(WorldPosition);
|
||||||
|
Handle->SetWorldScale3D(FVector(HandleScale));
|
||||||
|
|
||||||
|
// Collision for raycast
|
||||||
|
Handle->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||||
|
Handle->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||||
|
Handle->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
|
||||||
|
|
||||||
|
Handle->SetCastShadow(false);
|
||||||
|
Handle->SetRenderInDepthPass(false);
|
||||||
|
|
||||||
|
if (HandleMat)
|
||||||
|
{
|
||||||
|
Handle->SetMaterial(0, HandleMat);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::RebuildHandles()
|
||||||
|
{
|
||||||
|
// Destroy existing handles
|
||||||
|
for (UStaticMeshComponent* Handle : PointHandles)
|
||||||
|
{
|
||||||
|
if (Handle)
|
||||||
|
{
|
||||||
|
Handle->DestroyComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PointHandles.Empty();
|
||||||
|
|
||||||
|
// Create new handles at each point
|
||||||
|
const int32 Num = GetNumPoints();
|
||||||
|
for (int32 i = 0; i < Num; i++)
|
||||||
|
{
|
||||||
|
UStaticMeshComponent* Handle = CreatePointHandle(GetPointLocation(i));
|
||||||
|
PointHandles.Add(Handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 APS_Editor_SplineActor::GetHandleIndexUnderCursor(const FVector& RayOrigin, const FVector& RayDirection, float SphereRadius) const
|
||||||
|
{
|
||||||
|
int32 BestIndex = -1;
|
||||||
|
float BestDist = MAX_FLT;
|
||||||
|
|
||||||
|
for (int32 i = 0; i < PointHandles.Num(); i++)
|
||||||
|
{
|
||||||
|
if (!PointHandles[i] || !PointHandles[i]->IsVisible()) continue;
|
||||||
|
|
||||||
|
const FVector HandlePos = PointHandles[i]->GetComponentLocation();
|
||||||
|
|
||||||
|
// Sphere-ray intersection
|
||||||
|
const FVector ToHandle = HandlePos - RayOrigin;
|
||||||
|
const float Proj = FVector::DotProduct(ToHandle, RayDirection);
|
||||||
|
if (Proj < 0.0f) continue; // Behind camera
|
||||||
|
|
||||||
|
const FVector ClosestPoint = RayOrigin + RayDirection * Proj;
|
||||||
|
const float Dist = FVector::Dist(ClosestPoint, HandlePos);
|
||||||
|
|
||||||
|
if (Dist < SphereRadius && Proj < BestDist)
|
||||||
|
{
|
||||||
|
BestDist = Proj;
|
||||||
|
BestIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return BestIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::SetHandlesVisible(bool bVisible)
|
||||||
|
{
|
||||||
|
for (UStaticMeshComponent* Handle : PointHandles)
|
||||||
|
{
|
||||||
|
if (Handle)
|
||||||
|
{
|
||||||
|
Handle->SetVisibility(bVisible);
|
||||||
|
Handle->SetCollisionEnabled(bVisible ? ECollisionEnabled::QueryOnly : ECollisionEnabled::NoCollision);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::HighlightHandle(int32 Index)
|
||||||
|
{
|
||||||
|
for (int32 i = 0; i < PointHandles.Num(); i++)
|
||||||
|
{
|
||||||
|
if (PointHandles[i])
|
||||||
|
{
|
||||||
|
PointHandles[i]->SetMaterial(0, (i == Index) ? HandleHighlightMat : HandleMat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::ClearHandleHighlight()
|
||||||
|
{
|
||||||
|
for (UStaticMeshComponent* Handle : PointHandles)
|
||||||
|
{
|
||||||
|
if (Handle && HandleMat)
|
||||||
|
{
|
||||||
|
Handle->SetMaterial(0, HandleMat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::SetSelected(bool bSelected)
|
||||||
|
{
|
||||||
|
// Swap line material: depth-tested (normal) vs no-depth-test (on top when selected)
|
||||||
|
if (SplineMeshComp)
|
||||||
|
{
|
||||||
|
UMaterialInstanceDynamic* Mat = bSelected ? SplineMatSelected : SplineMat;
|
||||||
|
if (Mat)
|
||||||
|
{
|
||||||
|
SplineMeshComp->SetMaterial(0, Mat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center cube: bright when selected
|
||||||
|
if (CenterCubeMat)
|
||||||
|
{
|
||||||
|
const FLinearColor Color = bSelected ? FLinearColor(1.0f, 0.8f, 0.0f) : FLinearColor(1.0f, 0.4f, 0.0f);
|
||||||
|
CenterCubeMat->SetVectorParameterValue(TEXT("Color"), Color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool APS_Editor_SplineActor::IsCenterCube(const UPrimitiveComponent* Comp) const
|
||||||
|
{
|
||||||
|
return Comp && Comp == CenterCube;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector APS_Editor_SplineActor::GetCentroid() const
|
||||||
|
{
|
||||||
|
const int32 Num = GetNumPoints();
|
||||||
|
if (Num == 0) return GetActorLocation();
|
||||||
|
|
||||||
|
FVector Centroid = FVector::ZeroVector;
|
||||||
|
for (int32 i = 0; i < Num; i++)
|
||||||
|
{
|
||||||
|
Centroid += GetPointLocation(i);
|
||||||
|
}
|
||||||
|
return Centroid / Num;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Serialization ----
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::ExportToCustomProperties(TMap<FString, FString>& OutProperties) const
|
||||||
|
{
|
||||||
|
const int32 Num = GetNumPoints();
|
||||||
|
OutProperties.Add(TEXT("SplinePointCount"), FString::FromInt(Num));
|
||||||
|
|
||||||
|
for (int32 i = 0; i < Num; i++)
|
||||||
|
{
|
||||||
|
const FVector Loc = GetPointLocation(i);
|
||||||
|
OutProperties.Add(
|
||||||
|
FString::Printf(TEXT("SplinePoint_%d"), i),
|
||||||
|
FString::Printf(TEXT("X=%.4f Y=%.4f Z=%.4f"), Loc.X, Loc.Y, Loc.Z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_SplineActor::ImportFromCustomProperties(const TMap<FString, FString>& Properties)
|
||||||
|
{
|
||||||
|
const FString* CountStr = Properties.Find(TEXT("SplinePointCount"));
|
||||||
|
if (!CountStr) return;
|
||||||
|
|
||||||
|
const int32 Num = FCString::Atoi(**CountStr);
|
||||||
|
if (Num < 2) return;
|
||||||
|
|
||||||
|
TArray<FVector> Points;
|
||||||
|
for (int32 i = 0; i < Num; i++)
|
||||||
|
{
|
||||||
|
const FString Key = FString::Printf(TEXT("SplinePoint_%d"), i);
|
||||||
|
const FString* ValStr = Properties.Find(Key);
|
||||||
|
if (!ValStr) continue;
|
||||||
|
|
||||||
|
FVector Loc;
|
||||||
|
if (Loc.InitFromString(*ValStr))
|
||||||
|
{
|
||||||
|
Points.Add(Loc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Points.Num() >= 2)
|
||||||
|
{
|
||||||
|
SetAllPointLocations(Points);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "PS_Editor_SplineActor.generated.h"
|
||||||
|
|
||||||
|
class USplineComponent;
|
||||||
|
class UProceduralMeshComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime spline actor for path/route editing in PS_Editor.
|
||||||
|
* Renders as a visible quad strip with clickable point handles.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class PS_EDITOR_API APS_Editor_SplineActor : public AActor
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
APS_Editor_SplineActor();
|
||||||
|
|
||||||
|
// ---- Point manipulation ----
|
||||||
|
|
||||||
|
/** Add a point at the end of the spline (world space). */
|
||||||
|
void AddPoint(const FVector& WorldPosition);
|
||||||
|
|
||||||
|
/** Move an existing point to a new world position. */
|
||||||
|
void MovePoint(int32 Index, const FVector& NewWorldPosition);
|
||||||
|
|
||||||
|
/** Remove a point by index. */
|
||||||
|
void RemovePoint(int32 Index);
|
||||||
|
|
||||||
|
/** Get the number of spline points. */
|
||||||
|
int32 GetNumPoints() const;
|
||||||
|
|
||||||
|
/** Get the world position of a spline point. */
|
||||||
|
FVector GetPointLocation(int32 Index) const;
|
||||||
|
|
||||||
|
/** Get all point world positions (for undo snapshots). */
|
||||||
|
TArray<FVector> GetAllPointLocations() const;
|
||||||
|
|
||||||
|
/** Set all point positions at once (for undo restore). Rebuilds handles and visualization. */
|
||||||
|
void SetAllPointLocations(const TArray<FVector>& Points);
|
||||||
|
|
||||||
|
// ---- Visualization ----
|
||||||
|
|
||||||
|
/** Regenerate the line mesh and reposition all handles. */
|
||||||
|
void UpdateVisualization();
|
||||||
|
|
||||||
|
// ---- Handle interaction ----
|
||||||
|
|
||||||
|
/** Find which point handle is under the cursor ray. Returns -1 if none. */
|
||||||
|
int32 GetHandleIndexUnderCursor(const FVector& RayOrigin, const FVector& RayDirection, float SphereRadius = 15.0f) const;
|
||||||
|
|
||||||
|
/** Show or hide all point handles and center cube. */
|
||||||
|
void SetHandlesVisible(bool bVisible);
|
||||||
|
|
||||||
|
/** Set the spline's selected state (changes line + cube color). */
|
||||||
|
void SetSelected(bool bSelected);
|
||||||
|
|
||||||
|
/** Returns true if the given component is the center cube. */
|
||||||
|
bool IsCenterCube(const UPrimitiveComponent* Comp) const;
|
||||||
|
|
||||||
|
/** Highlight a specific handle (yellow), others go back to normal (white). */
|
||||||
|
void HighlightHandle(int32 Index);
|
||||||
|
|
||||||
|
/** Clear all handle highlights. */
|
||||||
|
void ClearHandleHighlight();
|
||||||
|
|
||||||
|
// ---- Serialization ----
|
||||||
|
|
||||||
|
/** Get the centroid of all spline points (world space). Used as pivot for gizmo. */
|
||||||
|
FVector GetCentroid() const;
|
||||||
|
|
||||||
|
/** Export spline data to a key-value map (for JSON serialization). */
|
||||||
|
void ExportToCustomProperties(TMap<FString, FString>& OutProperties) const;
|
||||||
|
|
||||||
|
/** Import spline data from a key-value map (for scene loading). */
|
||||||
|
void ImportFromCustomProperties(const TMap<FString, FString>& Properties);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void BeginPlay() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
UPROPERTY(VisibleAnywhere)
|
||||||
|
TObjectPtr<USplineComponent> SplineComp;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere)
|
||||||
|
TObjectPtr<UProceduralMeshComponent> SplineMeshComp;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere)
|
||||||
|
TObjectPtr<USceneComponent> HandleRoot;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TArray<TObjectPtr<UStaticMeshComponent>> PointHandles;
|
||||||
|
|
||||||
|
// Materials
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UMaterialInstanceDynamic> SplineMat; // Normal (depth tested)
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UMaterialInstanceDynamic> SplineMatSelected; // Selected (no depth test, on top)
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UMaterialInstanceDynamic> HandleMat;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UMaterialInstanceDynamic> HandleHighlightMat;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UStaticMeshComponent> CenterCube;
|
||||||
|
|
||||||
|
/** Create a handle sphere for a point at the given world position. */
|
||||||
|
UStaticMeshComponent* CreatePointHandle(const FVector& WorldPosition);
|
||||||
|
|
||||||
|
/** Rebuild all handles from current spline points. */
|
||||||
|
void RebuildHandles();
|
||||||
|
|
||||||
|
/** Update center cube position to the centroid of all points. */
|
||||||
|
void UpdateCenterCube();
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UMaterialInstanceDynamic> CenterCubeMat;
|
||||||
|
|
||||||
|
/** Line rendering settings. */
|
||||||
|
float LineHalfWidth = 2.0f;
|
||||||
|
float SampleInterval = 10.0f;
|
||||||
|
float HandleScale = 0.12f;
|
||||||
|
float CenterCubeScale = 0.10f;
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "PS_Editor_EditableComponent.h"
|
#include "PS_Editor_EditableComponent.h"
|
||||||
#include "PS_Editor_SpawnableComponent.h"
|
#include "PS_Editor_SpawnableComponent.h"
|
||||||
|
#include "PS_Editor_SplineActor.h"
|
||||||
#include "Widgets/SBoxPanel.h"
|
#include "Widgets/SBoxPanel.h"
|
||||||
#include "Widgets/Text/STextBlock.h"
|
#include "Widgets/Text/STextBlock.h"
|
||||||
#include "Widgets/Layout/SBorder.h"
|
#include "Widgets/Layout/SBorder.h"
|
||||||
@@ -239,6 +240,31 @@ 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->FinishSplinePlacement();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EditorPC->BeginSplinePlacement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FReply::Handled();
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("New Spline (S)")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||||
|
]
|
||||||
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -685,14 +711,28 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
|
|||||||
// Update mode text
|
// Update mode text
|
||||||
if (ModeText.IsValid())
|
if (ModeText.IsValid())
|
||||||
{
|
{
|
||||||
FString ModeStr;
|
// Check for spline placement mode first
|
||||||
switch (SM->GetTransformMode())
|
bool bSplineMode = false;
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
{
|
{
|
||||||
case EPS_Editor_TransformMode::Translate: ModeStr = TEXT("Translate (Z)"); break;
|
if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
case EPS_Editor_TransformMode::Rotate: ModeStr = TEXT("Rotate (E)"); break;
|
{
|
||||||
case EPS_Editor_TransformMode::Scale: ModeStr = TEXT("Scale (R)"); break;
|
ModeText->SetText(FText::FromString(TEXT("Spline Placement (click to add, RMB/Enter to finish, Esc to cancel)")));
|
||||||
|
bSplineMode = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bSplineMode)
|
||||||
|
{
|
||||||
|
FString ModeStr;
|
||||||
|
switch (SM->GetTransformMode())
|
||||||
|
{
|
||||||
|
case EPS_Editor_TransformMode::Translate: ModeStr = TEXT("Translate (Z)"); break;
|
||||||
|
case EPS_Editor_TransformMode::Rotate: ModeStr = TEXT("Rotate (E)"); break;
|
||||||
|
case EPS_Editor_TransformMode::Scale: ModeStr = TEXT("Scale (R)"); break;
|
||||||
|
}
|
||||||
|
ModeText->SetText(FText::FromString(ModeStr));
|
||||||
}
|
}
|
||||||
ModeText->SetText(FText::FromString(ModeStr));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update space button text
|
// Update space button text
|
||||||
@@ -877,16 +917,17 @@ void UPS_Editor_MainWidget::RefreshOutliner()
|
|||||||
AActor* Actor = SpawnedActors[i].Get();
|
AActor* Actor = SpawnedActors[i].Get();
|
||||||
if (!Actor || Actor->IsHidden()) continue;
|
if (!Actor || Actor->IsHidden()) continue;
|
||||||
|
|
||||||
// Build display name: SpawnableComponent DisplayName > Class name (cleaned)
|
// Build display name
|
||||||
FString DisplayName;
|
FString DisplayName;
|
||||||
if (UActorComponent* SpawnComp = Actor->FindComponentByClass(UPS_Editor_SpawnableComponent::StaticClass()))
|
if (Cast<APS_Editor_SplineActor>(Actor))
|
||||||
{
|
{
|
||||||
if (UPS_Editor_SpawnableComponent* SC = Cast<UPS_Editor_SpawnableComponent>(SpawnComp))
|
DisplayName = TEXT("Spline");
|
||||||
|
}
|
||||||
|
else if (UPS_Editor_SpawnableComponent* SC = Actor->FindComponentByClass<UPS_Editor_SpawnableComponent>())
|
||||||
|
{
|
||||||
|
if (!SC->DisplayName.IsEmpty())
|
||||||
{
|
{
|
||||||
if (!SC->DisplayName.IsEmpty())
|
DisplayName = SC->DisplayName;
|
||||||
{
|
|
||||||
DisplayName = SC->DisplayName;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (DisplayName.IsEmpty())
|
if (DisplayName.IsEmpty())
|
||||||
|
|||||||
Reference in New Issue
Block a user