Compare commits

..

2 Commits

Author SHA1 Message Date
7f2600f0b8 ChildMovable: rotation support + ChildMovable-aware help bar + clear previous highlight
- IPS_Editor_ChildMovable gains GetChildRotation / RotateChild and the
  GetAll/SetAll rotation pair for undo snapshots. BP impls can clamp axes
  (e.g. Yaw-only) inside RotateChild.
- Pawn captures initial rotations at gizmo drag start and applies the
  rotate gizmo's delta-quat onto the active child only (no longer
  rotating every selected actor when a child handle is active).
- FPS_Editor_SplinePointAction carries optional Old/NewRotations arrays
  applied alongside positions on Undo/Redo for ChildMovable actors.
- Pawn now calls ClearChildHighlight before HighlightChild so picking a
  new child no longer leaves the previous one stuck in highlight state.
- MainWidget + Legacy widget detect when ActivePlacementActor is a
  ChildMovable in SplinePlacement mode and swap the help bar text
  ("CHILDREN: ...") + hide the irrelevant Delete-point button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 16:07:39 +02:00
c520f3ccd9 Group BP interface functions under "PS Editor|Editable" / "PS Editor|Child Movable"
The two BlueprintNativeEvent interfaces previously dumped every function
into a flat "PS Editor" category, so the BP class members panel listed
them all together. Use the pipe-separated category to put each interface
under its own submenu.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 13:44:25 +02:00
8 changed files with 154 additions and 35 deletions

View File

@@ -448,15 +448,31 @@ void APS_Editor_Pawn::HandleLook(const FInputActionValue& Value)
const FQuat RotationQuat(AxisDir, FMath::DegreesToRadians(SnapAngle)); const FQuat RotationQuat(AxisDir, FMath::DegreesToRadians(SnapAngle));
const FVector GizmoLoc = GizmoDragPlaneOrigin; const FVector GizmoLoc = GizmoDragPlaneOrigin;
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i) // ChildMovable rotation: rotate only the active child instead of all selected actors.
// We compose the delta quat onto the initial rotation captured at drag start —
// reading the live value each frame would feed back through GetChildRotation and
// drift if the BP impl clamps axes.
if (ActiveSplinePointIndex >= 0
&& DraggedSplineActor.IsValid()
&& DraggedSplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())
&& SplinePointDragInitialRotations.IsValidIndex(ActiveSplinePointIndex))
{ {
if (AActor* Actor = Selected[i].Get()) const FQuat OrigRot = SplinePointDragInitialRotations[ActiveSplinePointIndex].Quaternion();
const FRotator NewRot = (RotationQuat * OrigRot).Rotator();
IPS_Editor_ChildMovable::Execute_RotateChild(DraggedSplineActor.Get(), ActiveSplinePointIndex, NewRot);
}
else
{
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
{ {
const FVector Offset = GizmoDragInitialTransforms[i].GetLocation() - GizmoLoc; if (AActor* Actor = Selected[i].Get())
Actor->SetActorLocation(GizmoLoc + RotationQuat.RotateVector(Offset)); {
const FVector Offset = GizmoDragInitialTransforms[i].GetLocation() - GizmoLoc;
Actor->SetActorLocation(GizmoLoc + RotationQuat.RotateVector(Offset));
const FQuat OrigRot = GizmoDragInitialTransforms[i].GetRotation(); const FQuat OrigRot = GizmoDragInitialTransforms[i].GetRotation();
Actor->SetActorRotation((RotationQuat * OrigRot).Rotator()); Actor->SetActorRotation((RotationQuat * OrigRot).Rotator());
}
} }
} }
} }
@@ -680,6 +696,9 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
{ {
ActiveSplinePointIndex = ChildIdx; ActiveSplinePointIndex = ChildIdx;
DraggedSplineActor = ActiveActor; DraggedSplineActor = ActiveActor;
// Clear any previous highlight before applying the new one — without this, picking
// a different child leaves the prior one stuck in highlight state.
IPS_Editor_ChildMovable::Execute_ClearChildHighlight(ActiveActor);
IPS_Editor_ChildMovable::Execute_HighlightChild(ActiveActor, ChildIdx); IPS_Editor_ChildMovable::Execute_HighlightChild(ActiveActor, ChildIdx);
if (UPS_Editor_SelectionManager* SM = EdPC_CM->GetSelectionManager()) if (UPS_Editor_SelectionManager* SM = EdPC_CM->GetSelectionManager())
@@ -849,6 +868,7 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
{ {
TArray<FVector> NewPoints; TArray<FVector> NewPoints;
TArray<FRotator> NewRotations;
FString Desc; FString Desc;
if (IPS_Editor_SplineEditable* SplineActor = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get())) if (IPS_Editor_SplineEditable* SplineActor = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get()))
@@ -859,7 +879,16 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
else if (DraggedSplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) else if (DraggedSplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
{ {
NewPoints = IPS_Editor_ChildMovable::Execute_GetAllChildLocations(DraggedSplineActor.Get()); NewPoints = IPS_Editor_ChildMovable::Execute_GetAllChildLocations(DraggedSplineActor.Get());
Desc = TEXT("Move child element"); // Snapshot rotations so the undo system restores both translation and rotation.
// The arrays may be empty if the BP didn't implement rotation — that's fine,
// the undo Execute/Undo only applies when both old and new are non-empty.
NewRotations = IPS_Editor_ChildMovable::Execute_GetAllChildRotations(DraggedSplineActor.Get());
bool bIsRotate = false;
if (UPS_Editor_SelectionManager* SMDesc = EditorPC->GetSelectionManager())
{
bIsRotate = (SMDesc->GetTransformMode() == EPS_Editor_TransformMode::Rotate);
}
Desc = bIsRotate ? TEXT("Rotate child element") : TEXT("Move child element");
} }
if (NewPoints.Num() > 0) if (NewPoints.Num() > 0)
@@ -868,6 +897,8 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
Action->SplineActor = DraggedSplineActor.Get(); Action->SplineActor = DraggedSplineActor.Get();
Action->OldPoints = SplinePointDragInitialState; Action->OldPoints = SplinePointDragInitialState;
Action->NewPoints = NewPoints; Action->NewPoints = NewPoints;
Action->OldRotations = SplinePointDragInitialRotations;
Action->NewRotations = NewRotations;
Action->Description = Desc; Action->Description = Desc;
UM->RecordAction(Action); UM->RecordAction(Action);
} }
@@ -1523,6 +1554,7 @@ void APS_Editor_Pawn::BeginGizmoDrag(EPS_Editor_GizmoAxis Axis)
} }
// Store initial sub-element state if editing a point/child // Store initial sub-element state if editing a point/child
SplinePointDragInitialRotations.Reset();
if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid()) if (ActiveSplinePointIndex >= 0 && DraggedSplineActor.IsValid())
{ {
if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get())) if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(DraggedSplineActor.Get()))
@@ -1532,6 +1564,9 @@ void APS_Editor_Pawn::BeginGizmoDrag(EPS_Editor_GizmoAxis Axis)
else if (DraggedSplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) else if (DraggedSplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
{ {
SplinePointDragInitialState = IPS_Editor_ChildMovable::Execute_GetAllChildLocations(DraggedSplineActor.Get()); SplinePointDragInitialState = IPS_Editor_ChildMovable::Execute_GetAllChildLocations(DraggedSplineActor.Get());
// Snapshot rotations too so the rotate gizmo can compute deltas from a fixed
// reference and the undo system can restore them.
SplinePointDragInitialRotations = IPS_Editor_ChildMovable::Execute_GetAllChildRotations(DraggedSplineActor.Get());
} }
} }

View File

@@ -209,9 +209,12 @@ private:
UPROPERTY() UPROPERTY()
TObjectPtr<UInputAction> IA_Cancel; TObjectPtr<UInputAction> IA_Cancel;
// ---- Spline point drag state ---- // ---- Spline point / ChildMovable drag state ----
TWeakObjectPtr<AActor> DraggedSplineActor; TWeakObjectPtr<AActor> DraggedSplineActor;
TArray<FVector> SplinePointDragInitialState; TArray<FVector> SplinePointDragInitialState;
// Initial rotations captured at drag start when dragging a ChildMovable sub-element.
// Empty for spline points (which do not have a rotation concept).
TArray<FRotator> SplinePointDragInitialRotations;
FVector SplinePointDragPlaneOrigin; FVector SplinePointDragPlaneOrigin;
FVector SplinePointDragPlaneNormal; FVector SplinePointDragPlaneNormal;

View File

@@ -13,6 +13,11 @@ void FPS_Editor_SplinePointAction::Execute()
else if (SplineActor.IsValid() && SplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) else if (SplineActor.IsValid() && SplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
{ {
IPS_Editor_ChildMovable::Execute_SetAllChildLocations(SplineActor.Get(), NewPoints); IPS_Editor_ChildMovable::Execute_SetAllChildLocations(SplineActor.Get(), NewPoints);
// Apply rotations only if the BP captured them (otherwise we'd reset to identity).
if (NewRotations.Num() > 0)
{
IPS_Editor_ChildMovable::Execute_SetAllChildRotations(SplineActor.Get(), NewRotations);
}
} }
} }
@@ -25,6 +30,10 @@ void FPS_Editor_SplinePointAction::Undo()
else if (SplineActor.IsValid() && SplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) else if (SplineActor.IsValid() && SplineActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass()))
{ {
IPS_Editor_ChildMovable::Execute_SetAllChildLocations(SplineActor.Get(), OldPoints); IPS_Editor_ChildMovable::Execute_SetAllChildLocations(SplineActor.Get(), OldPoints);
if (OldRotations.Num() > 0)
{
IPS_Editor_ChildMovable::Execute_SetAllChildRotations(SplineActor.Get(), OldRotations);
}
} }
} }

View File

@@ -170,6 +170,11 @@ struct FPS_Editor_SplinePointAction : public FPS_Editor_Action
TWeakObjectPtr<AActor> SplineActor; TWeakObjectPtr<AActor> SplineActor;
TArray<FVector> OldPoints; TArray<FVector> OldPoints;
TArray<FVector> NewPoints; TArray<FVector> NewPoints;
// ChildMovable-only: rotation snapshots. Empty for spline points and for ChildMovable BPs
// that don't implement rotation. Execute/Undo only apply rotations when both arrays match
// the points array length.
TArray<FRotator> OldRotations;
TArray<FRotator> NewRotations;
FString Description; FString Description;
virtual void Execute() override; virtual void Execute() override;

View File

@@ -25,47 +25,67 @@ class PS_EDITOR_API IPS_Editor_ChildMovable
public: public:
/** Number of movable sub-elements. */ /** Number of movable sub-elements. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
int32 GetMovableChildCount() const; int32 GetMovableChildCount() const;
/** World location of the sub-element at the given index. */ /** World location of the sub-element at the given index. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
FVector GetChildLocation(int32 Index) const; FVector GetChildLocation(int32 Index) const;
/** Move the sub-element at the given index to a new world position. */ /** Move the sub-element at the given index to a new world position. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
void MoveChild(int32 Index, const FVector& NewWorldPosition); void MoveChild(int32 Index, const FVector& NewWorldPosition);
/** Return all sub-element positions (used for undo snapshots). */ /** Return all sub-element positions (used for undo snapshots). */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
TArray<FVector> GetAllChildLocations() const; TArray<FVector> GetAllChildLocations() const;
/** Restore all sub-element positions (used for undo). */ /** Restore all sub-element positions (used for undo). */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
void SetAllChildLocations(const TArray<FVector>& Positions); void SetAllChildLocations(const TArray<FVector>& Positions);
/** World rotation of the sub-element at the given index. Implement to constrain the
* rotation axes you actually expose (e.g. yaw only) — the plugin gizmo writes whatever
* the user drags, so any clamping must happen in RotateChild on the BP side. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
FRotator GetChildRotation(int32 Index) const;
/** Apply a new world rotation to the sub-element at the given index. The BP impl is the
* right place to drop unsupported axes (e.g. keep only Yaw, force Pitch/Roll to zero). */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
void RotateChild(int32 Index, const FRotator& NewWorldRotation);
/** Return all sub-element rotations (used for undo snapshots). Empty array allowed if
* rotation is not supported on this actor — the undo system then ignores rotation. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
TArray<FRotator> GetAllChildRotations() const;
/** Restore all sub-element rotations (used for undo). */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
void SetAllChildRotations(const TArray<FRotator>& Rotations);
/** Index of the sub-element under the cursor ray (-1 if none). /** Index of the sub-element under the cursor ray (-1 if none).
* Implement with sphere-ray intersection or any hit detection that suits your visuals. */ * Implement with sphere-ray intersection or any hit detection that suits your visuals. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
int32 GetChildIndexUnderCursor(const FVector& RayOrigin, const FVector& RayDirection) const; int32 GetChildIndexUnderCursor(const FVector& RayOrigin, const FVector& RayDirection) const;
/** Show or hide visual handles for the sub-elements. */ /** Show or hide visual handles for the sub-elements. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
void SetChildHandlesVisible(bool bVisible); void SetChildHandlesVisible(bool bVisible);
/** Highlight a specific sub-element (e.g. change handle color). */ /** Highlight a specific sub-element (e.g. change handle color). */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
void HighlightChild(int32 Index); void HighlightChild(int32 Index);
/** Clear all sub-element highlights. */ /** Clear all sub-element highlights. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
void ClearChildHighlight(); void ClearChildHighlight();
/** Export sub-element positions to the CustomProperties map for scene saving. */ /** Export sub-element positions to the CustomProperties map for scene saving. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
void ExportChildLocations(TMap<FString, FString>& OutProperties) const; void ExportChildLocations(TMap<FString, FString>& OutProperties) const;
/** Import sub-element positions from the CustomProperties map on scene load. */ /** Import sub-element positions from the CustomProperties map on scene load. */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Child Movable")
void ImportChildLocations(const TMap<FString, FString>& Properties); void ImportChildLocations(const TMap<FString, FString>& Properties);
}; };

View File

@@ -51,21 +51,21 @@ public:
* The new value is already applied when this is called. * The new value is already applied when this is called.
* @param PropertyPath The property that changed (e.g. "CharacterType" or "MyComponent.SomeProperty"). * @param PropertyPath The property that changed (e.g. "CharacterType" or "MyComponent.SomeProperty").
*/ */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
void OnEditorPropertyChanged(const FString& PropertyPath); void OnEditorPropertyChanged(const FString& PropertyPath);
/** /**
* Called when a property with bNeedsReinit is changed. * Called when a property with bNeedsReinit is changed.
* The new value is already applied. Use this to re-run initialization logic. * The new value is already applied. Use this to re-run initialization logic.
*/ */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
void OnEditorNeedsReinit(); void OnEditorNeedsReinit();
/** /**
* Called on the NEW actor after a respawn triggered by bNeedsRespawn. * Called on the NEW actor after a respawn triggered by bNeedsRespawn.
* All properties have been applied. Use this for post-respawn setup. * All properties have been applied. Use this for post-respawn setup.
*/ */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
void OnEditorNeedsRespawn(); void OnEditorNeedsRespawn();
/** /**
@@ -73,7 +73,7 @@ public:
* Fires both in editor (SceneSerializer) and in gameplay (SceneLoader). * Fires both in editor (SceneSerializer) and in gameplay (SceneLoader).
* Use this to re-initialize your actor based on the loaded property values. * Use this to re-initialize your actor based on the loaded property values.
*/ */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
void OnPropertiesLoaded(); void OnPropertiesLoaded();
/** /**
@@ -81,7 +81,7 @@ public:
* Use this to show/hide editor-only helpers (VR guides, placement aids, debug visuals, etc.). * Use this to show/hide editor-only helpers (VR guides, placement aids, debug visuals, etc.).
* @param bIsEditing True when entering edit mode, false when leaving (Play, return to game). * @param bIsEditing True when entering edit mode, false when leaving (Play, return to game).
*/ */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
void OnEditorModeChanged(bool bIsEditing); void OnEditorModeChanged(bool bIsEditing);
/** /**
@@ -90,7 +90,7 @@ public:
* the hood). Use this to kick off behaviour that should only happen while AI is live * the hood). Use this to kick off behaviour that should only happen while AI is live
* (spawn FX, reset state counters, unlock input, etc.). * (spawn FX, reset state counters, unlock input, etc.).
*/ */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
void OnEditorSimulateStart(); void OnEditorSimulateStart();
/** /**
@@ -102,7 +102,7 @@ public:
* Timing: fires at the "AI cut" moment, AFTER any grace period the timeline applied * Timing: fires at the "AI cut" moment, AFTER any grace period the timeline applied
* (typically 500ms after Stop was clicked). BT is still live but about to be disabled. * (typically 500ms after Stop was clicked). BT is still live but about to be disabled.
*/ */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
void OnEditorSimulateStop(); void OnEditorSimulateStop();
/** /**
@@ -130,7 +130,7 @@ public:
* event still fires + grace period still granted) * event still fires + grace period still granted)
* Does NOT fire for direct StopSimulation calls (rare — e.g. forced editor close). * Does NOT fire for direct StopSimulation calls (rare — e.g. forced editor close).
*/ */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
void OnEditorBeforeSimulateStop(); void OnEditorBeforeSimulateStop();
/** /**
@@ -138,7 +138,7 @@ public:
* Use this to update visuals dynamically based on property changes, animate helpers, etc. * Use this to update visuals dynamically based on property changes, animate helpers, etc.
* @param DeltaTime Time since last frame. * @param DeltaTime Time since last frame.
*/ */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
void OnEditorTick(float DeltaTime); void OnEditorTick(float DeltaTime);
/** /**
@@ -150,7 +150,7 @@ public:
* @return List of option strings. Empty = use default widget / fall through to the * @return List of option strings. Empty = use default widget / fall through to the
* structured variant below. * structured variant below.
*/ */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
TArray<FString> GetCustomOptionsForProperty(const FString& PropertyPath) const; TArray<FString> GetCustomOptionsForProperty(const FString& PropertyPath) const;
/** /**
@@ -164,7 +164,7 @@ public:
* @param PropertyPath The property path (e.g. "WeaponName" or "MyComponent.WeaponType"). * @param PropertyPath The property path (e.g. "WeaponName" or "MyComponent.WeaponType").
* @return List of display/value pairs. Empty = fall back to the flat-string variant. * @return List of display/value pairs. Empty = fall back to the flat-string variant.
*/ */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
TArray<FPS_Editor_PropertyOption> GetCustomOptionsWithValuesForProperty(const FString& PropertyPath) const; TArray<FPS_Editor_PropertyOption> GetCustomOptionsWithValuesForProperty(const FString& PropertyPath) const;
/** /**
@@ -185,6 +185,6 @@ public:
* @param PropertyPath The property path (e.g. "Weapon" or "MyComponent.WeaponType"). * @param PropertyPath The property path (e.g. "Weapon" or "MyComponent.WeaponType").
* @return DisplayName to show in the combo, or empty string to fall back. * @return DisplayName to show in the combo, or empty string to fall back.
*/ */
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS_Editor") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor|Editable")
FString GetCurrentDisplayForProperty(const FString& PropertyPath) const; FString GetCurrentDisplayForProperty(const FString& PropertyPath) const;
}; };

View File

@@ -1821,9 +1821,33 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
bPointSelected = (Pawn->ActiveSplinePointIndex >= 0); bPointSelected = (Pawn->ActiveSplinePointIndex >= 0);
} }
// SplinePlacement mode is shared between IPS_Editor_SplineEditable and
// IPS_Editor_ChildMovable — show ChildMovable-specific help when the active
// placement actor implements the child interface (no point insertion, no Del,
// rotation gizmo available via R key).
bool bChildMovableActive = false;
if (APS_Editor_PlayerController* EditorPC2 = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
{
if (AActor* ActiveActor = EditorPC2->GetActivePlacementActor())
{
bChildMovableActive = ActiveActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass());
}
}
if (HelpBarText.IsValid()) if (HelpBarText.IsValid())
{ {
if (bPointSelected) if (bChildMovableActive)
{
if (bPointSelected)
{
HelpBarText->SetText(FText::FromString(TEXT("CHILDREN: Drag gizmo to move | R: rotate | T: translate | Esc/Done: finish")));
}
else
{
HelpBarText->SetText(FText::FromString(TEXT("CHILDREN: Click a child to select | Esc/Done: finish")));
}
}
else if (bPointSelected)
{ {
HelpBarText->SetText(FText::FromString(TEXT("SPLINE: Drag gizmo to move | Del: delete point | Click line: insert | Click outside: add | Esc/Done: finish"))); HelpBarText->SetText(FText::FromString(TEXT("SPLINE: Drag gizmo to move | Del: delete point | Click line: insert | Click outside: add | Esc/Done: finish")));
} }
@@ -1835,7 +1859,8 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
} }
if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed); if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed);
if (ChildEditButton.IsValid()) ChildEditButton->SetVisibility(EVisibility::Collapsed); if (ChildEditButton.IsValid()) ChildEditButton->SetVisibility(EVisibility::Collapsed);
if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(bPointSelected ? EVisibility::Visible : EVisibility::Collapsed); // Delete-point button is meaningless for ChildMovable (fixed child set)
if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility((bPointSelected && !bChildMovableActive) ? EVisibility::Visible : EVisibility::Collapsed);
if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Visible); if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Visible);
} }
else if (bChildMovableSelected) else if (bChildMovableSelected)

View File

@@ -1573,9 +1573,30 @@ void UPS_Editor_MainWidget_Legacy::RefreshPropertiesFromSelection()
bPointSelected = (Pawn->ActiveSplinePointIndex >= 0); bPointSelected = (Pawn->ActiveSplinePointIndex >= 0);
} }
// SplinePlacement mode is shared with IPS_Editor_ChildMovable — show the right help.
bool bChildMovableActive = false;
if (APS_Editor_PlayerController* EditorPC2 = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
{
if (AActor* ActiveActor = EditorPC2->GetActivePlacementActor())
{
bChildMovableActive = ActiveActor->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass());
}
}
if (HelpBarText.IsValid()) if (HelpBarText.IsValid())
{ {
if (bPointSelected) if (bChildMovableActive)
{
if (bPointSelected)
{
HelpBarText->SetText(FText::FromString(TEXT("CHILDREN: Drag gizmo to move | R: rotate | T: translate | Esc/Done: finish")));
}
else
{
HelpBarText->SetText(FText::FromString(TEXT("CHILDREN: Click a child to select | Esc/Done: finish")));
}
}
else if (bPointSelected)
{ {
HelpBarText->SetText(FText::FromString(TEXT("SPLINE: Drag gizmo to move | Del: delete point | Click line: insert | Click outside: add | Esc/Done: finish"))); HelpBarText->SetText(FText::FromString(TEXT("SPLINE: Drag gizmo to move | Del: delete point | Click line: insert | Click outside: add | Esc/Done: finish")));
} }
@@ -1587,7 +1608,8 @@ void UPS_Editor_MainWidget_Legacy::RefreshPropertiesFromSelection()
} }
if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed); if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed);
if (ChildEditButton.IsValid()) ChildEditButton->SetVisibility(EVisibility::Collapsed); if (ChildEditButton.IsValid()) ChildEditButton->SetVisibility(EVisibility::Collapsed);
if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(bPointSelected ? EVisibility::Visible : EVisibility::Collapsed); // Delete-point is meaningless for ChildMovable (fixed child set)
if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility((bPointSelected && !bChildMovableActive) ? EVisibility::Visible : EVisibility::Collapsed);
if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Visible); if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Visible);
} }
else if (bChildMovableSelected) else if (bChildMovableSelected)