// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved. #include "ComponentVisualizersPrivatePCH.h" #include "SplineComponentVisualizer.h" #include "ScopedTransaction.h" IMPLEMENT_HIT_PROXY(HSplineVisProxy, HComponentVisProxy); IMPLEMENT_HIT_PROXY(HSplineKeyProxy, HSplineVisProxy); IMPLEMENT_HIT_PROXY(HSplineSegmentProxy, HSplineVisProxy); #define LOCTEXT_NAMESPACE "SplineComponentVisualizer" /** Define commands for the spline component visualizer */ class FSplineComponentVisualizerCommands : public TCommands { public: FSplineComponentVisualizerCommands() : TCommands ( "SplineComponentVisualizer", // Context name for fast lookup LOCTEXT("SplineComponentVisualizer", "Spline Component Visualizer"), // Localized context name for displaying NAME_None, // Parent FEditorStyle::GetStyleSetName() ) { } virtual void RegisterCommands() override { UI_COMMAND(DeleteKey, "Delete Spline Point", "Delete the currently selected spline point.", EUserInterfaceActionType::Button, FInputGesture(EKeys::Delete)); UI_COMMAND(DuplicateKey, "Duplicate Spline Point", "Duplicate the currently selected spline point.", EUserInterfaceActionType::Button, FInputGesture()); UI_COMMAND(AddKey, "Add Spline Point Here", "Add a new spline point at the cursor location.", EUserInterfaceActionType::Button, FInputGesture()); UI_COMMAND(ResetToUnclampedTangent, "Unclamped Tangent", "Reset the tangent for this spline point to its default unclamped value.", EUserInterfaceActionType::Button, FInputGesture()); UI_COMMAND(ResetToClampedTangent, "Clamped Tangent", "Reset the tangent for this spline point to its default clamped value.", EUserInterfaceActionType::Button, FInputGesture()); UI_COMMAND(SetKeyToCurve, "Curve", "Set spline point to Curve type", EUserInterfaceActionType::RadioButton, FInputGesture()); UI_COMMAND(SetKeyToLinear, "Linear", "Set spline point to Linear type", EUserInterfaceActionType::RadioButton, FInputGesture()); UI_COMMAND(SetKeyToConstant, "Constant", "Set spline point to Constant type", EUserInterfaceActionType::RadioButton, FInputGesture()); } public: /** Delete key */ TSharedPtr DeleteKey; /** Duplicate key */ TSharedPtr DuplicateKey; /** Add key */ TSharedPtr AddKey; /** Reset to unclamped tangent */ TSharedPtr ResetToUnclampedTangent; /** Reset to clamped tangent */ TSharedPtr ResetToClampedTangent; /** Set spline key to Curve type */ TSharedPtr SetKeyToCurve; /** Set spline key to Linear type */ TSharedPtr SetKeyToLinear; /** Set spline key to Constant type */ TSharedPtr SetKeyToConstant; }; FSplineComponentVisualizer::FSplineComponentVisualizer() : FComponentVisualizer() , SelectedKeyIndex(INDEX_NONE) , SelectedSegmentIndex(INDEX_NONE) , bAllowDuplication(true) { FSplineComponentVisualizerCommands::Register(); SplineComponentVisualizerActions = MakeShareable(new FUICommandList); } void FSplineComponentVisualizer::OnRegister() { const auto& Commands = FSplineComponentVisualizerCommands::Get(); SplineComponentVisualizerActions->MapAction( Commands.DeleteKey, FExecuteAction::CreateSP(this, &FSplineComponentVisualizer::OnDeleteKey), FCanExecuteAction::CreateSP(this, &FSplineComponentVisualizer::IsKeySelectionValid)); SplineComponentVisualizerActions->MapAction( Commands.DuplicateKey, FExecuteAction::CreateSP(this, &FSplineComponentVisualizer::OnDuplicateKey), FCanExecuteAction::CreateSP(this, &FSplineComponentVisualizer::IsKeySelectionValid)); SplineComponentVisualizerActions->MapAction( Commands.AddKey, FExecuteAction::CreateSP(this, &FSplineComponentVisualizer::OnAddKey), FCanExecuteAction::CreateSP(this, &FSplineComponentVisualizer::CanAddKey)); SplineComponentVisualizerActions->MapAction( Commands.ResetToUnclampedTangent, FExecuteAction::CreateSP(this, &FSplineComponentVisualizer::OnResetToAutomaticTangent, CIM_CurveAuto), FCanExecuteAction::CreateSP(this, &FSplineComponentVisualizer::CanResetToAutomaticTangent, CIM_CurveAuto)); SplineComponentVisualizerActions->MapAction( Commands.ResetToClampedTangent, FExecuteAction::CreateSP(this, &FSplineComponentVisualizer::OnResetToAutomaticTangent, CIM_CurveAutoClamped), FCanExecuteAction::CreateSP(this, &FSplineComponentVisualizer::CanResetToAutomaticTangent, CIM_CurveAutoClamped)); SplineComponentVisualizerActions->MapAction( Commands.SetKeyToCurve, FExecuteAction::CreateSP(this, &FSplineComponentVisualizer::OnSetKeyType, CIM_CurveAuto), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &FSplineComponentVisualizer::IsKeyTypeSet, CIM_CurveAuto)); SplineComponentVisualizerActions->MapAction( Commands.SetKeyToLinear, FExecuteAction::CreateSP(this, &FSplineComponentVisualizer::OnSetKeyType, CIM_Linear), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &FSplineComponentVisualizer::IsKeyTypeSet, CIM_Linear)); SplineComponentVisualizerActions->MapAction( Commands.SetKeyToConstant, FExecuteAction::CreateSP(this, &FSplineComponentVisualizer::OnSetKeyType, CIM_Constant), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &FSplineComponentVisualizer::IsKeyTypeSet, CIM_Constant)); } FSplineComponentVisualizer::~FSplineComponentVisualizer() { FSplineComponentVisualizerCommands::Unregister(); } void FSplineComponentVisualizer::DrawVisualization(const UActorComponent* Component, const FSceneView* View, FPrimitiveDrawInterface* PDI) { if (const USplineComponent* SplineComp = Cast(Component)) { if (!SplineComp->bAllowSplineEditingPerInstance) { return; } USplineComponent* EditedSplineComp = GetEditedSplineComponent(); const FColor NormalColor(255, 255, 255); const FColor SelectedColor(255, 0, 0); const float GrabHandleSize = 12.0f; const FInterpCurveVector& SplineInfo = SplineComp->SplineInfo; FVector OldKeyPos(0); float OldKeyTime = 0.f; for (int32 KeyIdx = 0; KeyIdxComponentToWorld.TransformPosition( SplineInfo.Eval(NewKeyTime, FVector::ZeroVector) ); const FColor KeyColor = (SplineComp == EditedSplineComp && KeyIdx == SelectedKeyIndex) ? SelectedColor : NormalColor; // Draw the keypoint PDI->SetHitProxy(new HSplineKeyProxy(Component, KeyIdx)); PDI->DrawPoint(NewKeyPos, KeyColor, GrabHandleSize, SDPG_Foreground); PDI->SetHitProxy(NULL); // If not the first keypoint, draw a line to the previous keypoint. if (KeyIdx>0) { const FColor LineColor = (SplineComp == EditedSplineComp && KeyIdx == SelectedKeyIndex + 1) ? SelectedColor : NormalColor; PDI->SetHitProxy(new HSplineSegmentProxy(Component, KeyIdx - 1)); // For constant interpolation - don't draw ticks - just draw dotted line. if (SplineInfo.Points[KeyIdx - 1].InterpMode == CIM_Constant) { DrawDashedLine(PDI, OldKeyPos, NewKeyPos, LineColor, 20, SDPG_World); } else { int32 NumSteps = FMath::CeilToInt((NewKeyTime - OldKeyTime) * 32.0f); float DrawSubstep = (NewKeyTime - OldKeyTime) / NumSteps; // Find position on first keyframe. float OldTime = OldKeyTime; FVector OldPos = OldKeyPos; // Then draw a line for each substep. for (int32 StepIdx = 1; StepIdxComponentToWorld.TransformPosition( SplineInfo.Eval(NewTime, FVector::ZeroVector) ); PDI->DrawLine(OldPos, NewPos, LineColor, SDPG_Foreground); OldTime = NewTime; OldPos = NewPos; } } PDI->SetHitProxy(NULL); } OldKeyTime = NewKeyTime; OldKeyPos = NewKeyPos; } } } bool FSplineComponentVisualizer::VisProxyHandleClick(FLevelEditorViewportClient* InViewportClient, HComponentVisProxy* VisProxy, const FViewportClick& Click) { bool bEditing = false; if(VisProxy && VisProxy->Component.IsValid()) { const USplineComponent* SplineComp = CastChecked(VisProxy->Component.Get()); SplineCompPropName = GetComponentPropertyName(SplineComp); if(SplineCompPropName != NAME_None) { SplineOwningActor = SplineComp->GetOwner(); if (VisProxy->IsA(HSplineKeyProxy::StaticGetType())) { HSplineKeyProxy* KeyProxy = (HSplineKeyProxy*)VisProxy; SelectedKeyIndex = KeyProxy->KeyIndex; SelectedSegmentIndex = INDEX_NONE; bEditing = true; } else if (VisProxy->IsA(HSplineSegmentProxy::StaticGetType())) { // Divide segment into subsegments and test each subsegment against ray representing click position and camera direction. // Closest encounter with the spline determines the spline position. const int32 NumSubdivisions = 16; HSplineSegmentProxy* SegmentProxy = (HSplineSegmentProxy*)VisProxy; SelectedKeyIndex = SegmentProxy->SegmentIndex; SelectedSegmentIndex = SegmentProxy->SegmentIndex; float SubsegmentStartKey = static_cast(SelectedSegmentIndex); FVector SubsegmentStart = SplineComp->ComponentToWorld.TransformPosition(SplineComp->SplineInfo.Eval(SubsegmentStartKey, FVector::ZeroVector)); float ClosestDistance = TNumericLimits::Max(); FVector BestLocation = SubsegmentStart; for (int32 Step = 1; Step < NumSubdivisions; Step++) { const float SubsegmentEndKey = SelectedSegmentIndex + Step / static_cast(NumSubdivisions); const FVector SubsegmentEnd = SplineComp->ComponentToWorld.TransformPosition(SplineComp->SplineInfo.Eval(SubsegmentEndKey, FVector::ZeroVector)); FVector SplineClosest; FVector RayClosest; FMath::SegmentDistToSegmentSafe(SubsegmentStart, SubsegmentEnd, Click.GetOrigin(), Click.GetOrigin() + Click.GetDirection() * 50000.0f, SplineClosest, RayClosest); const float Distance = FVector::DistSquared(SplineClosest, RayClosest); if (Distance < ClosestDistance) { ClosestDistance = Distance; BestLocation = SplineClosest; } SubsegmentStartKey = SubsegmentEndKey; SubsegmentStart = SubsegmentEnd; } SelectedSplinePosition = BestLocation; bEditing = true; } } else { SplineOwningActor = NULL; } } return bEditing; } USplineComponent* FSplineComponentVisualizer::GetEditedSplineComponent() const { return Cast(GetComponentFromPropertyName(SplineOwningActor.Get(), SplineCompPropName)); } bool FSplineComponentVisualizer::GetWidgetLocation(const FEditorViewportClient* ViewportClient, FVector& OutLocation) const { USplineComponent* SplineComp = GetEditedSplineComponent(); if (SplineComp && SelectedKeyIndex != INDEX_NONE) { if (SelectedKeyIndex < SplineComp->SplineInfo.Points.Num()) { OutLocation = SplineComp->ComponentToWorld.TransformPosition( SplineComp->SplineInfo.Points[SelectedKeyIndex].OutVal ); return true; } } return false; } bool FSplineComponentVisualizer::GetCustomInputCoordinateSystem(const FEditorViewportClient* ViewportClient, FMatrix& OutMatrix) const { if (ViewportClient->GetWidgetCoordSystemSpace() == COORD_Local) { USplineComponent* SplineComp = GetEditedSplineComponent(); if (SplineComp && SelectedKeyIndex != INDEX_NONE) { if (SelectedKeyIndex < SplineComp->SplineInfo.Points.Num()) { const auto& Point = SplineComp->SplineInfo.Points[SelectedKeyIndex]; const FVector Tangent = Point.ArriveTangent.SafeNormal(); const FVector Bitangent = (Tangent.Z == 1.0f) ? FVector(1.0f, 0.0f, 0.0f) : FVector(-Tangent.Y, Tangent.X, 0.0f).SafeNormal(); const FVector Normal = FVector::CrossProduct(Tangent, Bitangent); OutMatrix = FMatrix(Tangent, Bitangent, Normal, FVector::ZeroVector) * FQuatRotationTranslationMatrix(SplineComp->ComponentToWorld.GetRotation(), FVector::ZeroVector); return true; } } } return false; } bool FSplineComponentVisualizer::HandleInputDelta(FEditorViewportClient* ViewportClient, FViewport* Viewport, FVector& DeltaTranslate, FRotator& DeltaRotate, FVector& DeltaScale) { USplineComponent* SplineComp = GetEditedSplineComponent(); if( SplineComp && SelectedKeyIndex != INDEX_NONE && SelectedKeyIndex < SplineComp->SplineInfo.Points.Num() ) { if (ViewportClient->IsAltPressed() && bAllowDuplication) { OnDuplicateKey(); // Don't duplicate again until we release LMB bAllowDuplication = false; } FInterpCurvePoint& EditedPoint = SplineComp->SplineInfo.Points[SelectedKeyIndex]; if (!DeltaTranslate.IsZero()) { // Find key position in world space FVector CurrentWorldPos = SplineComp->ComponentToWorld.TransformPosition(EditedPoint.OutVal); // Move in world space FVector NewWorldPos = CurrentWorldPos + DeltaTranslate; // Convert back to local space EditedPoint.OutVal = SplineComp->ComponentToWorld.InverseTransformPosition(NewWorldPos); } if (!DeltaRotate.IsZero()) { // Set point tangent as user controlled EditedPoint.InterpMode = CIM_CurveUser; // Rotate tangent according to delta rotation const FVector NewTangent = DeltaRotate.RotateVector(EditedPoint.LeaveTangent); EditedPoint.LeaveTangent = NewTangent; EditedPoint.ArriveTangent = NewTangent; } if (!DeltaScale.IsZero()) { // Set point tangent as user controlled EditedPoint.InterpMode = CIM_CurveUser; // Break tangent into direction and length so we can change its scale (the 'tension') // independently of its direction. FVector Direction; float Length; EditedPoint.LeaveTangent.ToDirectionAndLength(Direction, Length); // Figure out which component has changed, and use it float DeltaScaleValue = (DeltaScale.X != 0.0f) ? DeltaScale.X : ((DeltaScale.Y != 0.0f) ? DeltaScale.Y : DeltaScale.Z); // Change scale, avoiding singularity by never allowing a scale of 0, hence preserving direction. Length += DeltaScaleValue * 10.0f; if (Length == 0.0f) { Length = SMALL_NUMBER; } const FVector NewTangent = Direction * Length; EditedPoint.LeaveTangent = NewTangent; EditedPoint.ArriveTangent = NewTangent; } NotifyComponentModified(); return true; } return false; } bool FSplineComponentVisualizer::HandleInputKey(FEditorViewportClient* ViewportClient, FViewport* Viewport, FKey Key, EInputEvent Event) { bool bHandled = false; if (Key == EKeys::LeftMouseButton && Event == IE_Released) { // Reset duplication flag on LMB release bAllowDuplication = true; } if (Event == IE_Pressed) { bHandled = SplineComponentVisualizerActions->ProcessCommandBindings(Key, FSlateApplication::Get().GetModifierKeys(), false); } return bHandled; } void FSplineComponentVisualizer::EndEditing() { SplineOwningActor = NULL; SplineCompPropName = NAME_None; SelectedKeyIndex = INDEX_NONE; } void FSplineComponentVisualizer::OnDuplicateKey() { const FScopedTransaction Transaction(LOCTEXT("DuplicateSplinePoint", "Duplicate Spline Point")); USplineComponent* SplineComp = GetEditedSplineComponent(); if (AActor* Owner = SplineComp->GetOwner()) { Owner->Modify(); } FInterpCurvePoint KeyToDupe = SplineComp->SplineInfo.Points[SelectedKeyIndex]; KeyToDupe.InterpMode = CIM_CurveAuto; int32 NewKeyIndex = SplineComp->SplineInfo.Points.Insert(KeyToDupe, SelectedKeyIndex); // move selection to 'next' key SelectedKeyIndex++; // Update Input value for all keys SplineComp->RefreshSplineInputs(); NotifyComponentModified(); } bool FSplineComponentVisualizer::CanAddKey() const { USplineComponent* SplineComp = GetEditedSplineComponent(); return (SplineComp && SelectedSegmentIndex != INDEX_NONE && SelectedSegmentIndex < SplineComp->SplineInfo.Points.Num() - 1); } void FSplineComponentVisualizer::OnAddKey() { const FScopedTransaction Transaction(LOCTEXT("AddSplinePoint", "Add Spline Point")); USplineComponent* SplineComp = GetEditedSplineComponent(); if (AActor* Owner = SplineComp->GetOwner()) { Owner->Modify(); } FInterpCurvePoint NewKey; NewKey.InVal = SelectedSegmentIndex; NewKey.OutVal = SplineComp->ComponentToWorld.InverseTransformPosition(SelectedSplinePosition); NewKey.InterpMode = CIM_CurveAuto; int32 NewKeyIndex = SplineComp->SplineInfo.Points.Insert(NewKey, SelectedSegmentIndex + 1); // move selection to 'next' key SelectedKeyIndex = SelectedSegmentIndex + 1; SelectedSegmentIndex = INDEX_NONE; // Update Input value for all keys SplineComp->RefreshSplineInputs(); NotifyComponentModified(); } void FSplineComponentVisualizer::OnDeleteKey() { const FScopedTransaction Transaction(LOCTEXT("DeleteSplinePoint", "Delete Spline Point")); USplineComponent* SplineComp = GetEditedSplineComponent(); if (AActor* Owner = SplineComp->GetOwner()) { Owner->Modify(); } SplineComp->SplineInfo.Points.RemoveAt(SelectedKeyIndex); SplineComp->RefreshSplineInputs(); // update input value for each key SelectedKeyIndex = INDEX_NONE; // deselect any keys SelectedSegmentIndex = INDEX_NONE; NotifyComponentModified(); } bool FSplineComponentVisualizer::IsKeySelectionValid() const { USplineComponent* SplineComp = GetEditedSplineComponent(); return (SplineComp && SelectedSegmentIndex == INDEX_NONE && SelectedKeyIndex != INDEX_NONE && SelectedKeyIndex < SplineComp->SplineInfo.Points.Num()); } void FSplineComponentVisualizer::OnResetToAutomaticTangent(EInterpCurveMode Mode) { USplineComponent* SplineComp = GetEditedSplineComponent(); if (SplineComp->SplineInfo.Points[SelectedKeyIndex].InterpMode != Mode) { const FScopedTransaction Transaction(LOCTEXT("ResetToAutomaticTangent", "Reset to Automatic Tangent")); if (AActor* Owner = SplineComp->GetOwner()) { Owner->Modify(); } SplineComp->SplineInfo.Points[SelectedKeyIndex].InterpMode = Mode; NotifyComponentModified(); } } bool FSplineComponentVisualizer::CanResetToAutomaticTangent(EInterpCurveMode Mode) const { USplineComponent* SplineComp = GetEditedSplineComponent(); return (SplineComp && SelectedKeyIndex != INDEX_NONE && SelectedKeyIndex < SplineComp->SplineInfo.Points.Num() && SplineComp->SplineInfo.Points[SelectedKeyIndex].IsCurveKey() && SplineComp->SplineInfo.Points[SelectedKeyIndex].InterpMode != Mode); } void FSplineComponentVisualizer::OnSetKeyType(EInterpCurveMode Mode) { USplineComponent* SplineComp = GetEditedSplineComponent(); if (SplineComp->SplineInfo.Points[SelectedKeyIndex].InterpMode != Mode) { const FScopedTransaction Transaction(LOCTEXT("SetSplinePointType", "Set Spline Point Type")); if (AActor* Owner = SplineComp->GetOwner()) { Owner->Modify(); } SplineComp->SplineInfo.Points[SelectedKeyIndex].InterpMode = Mode; NotifyComponentModified(); } } bool FSplineComponentVisualizer::IsKeyTypeSet(EInterpCurveMode Mode) const { if (IsKeySelectionValid()) { USplineComponent* SplineComp = GetEditedSplineComponent(); const auto& SelectedPoint = SplineComp->SplineInfo.Points[SelectedKeyIndex]; return ((Mode == CIM_CurveAuto && SelectedPoint.IsCurveKey()) || SelectedPoint.InterpMode == Mode); } return false; } TSharedPtr FSplineComponentVisualizer::GenerateContextMenu() const { FMenuBuilder MenuBuilder(true, SplineComponentVisualizerActions); { MenuBuilder.BeginSection("SplineKeyEdit"); { if (SelectedSegmentIndex != INDEX_NONE) { MenuBuilder.AddMenuEntry(FSplineComponentVisualizerCommands::Get().AddKey); } else if (SelectedKeyIndex != INDEX_NONE) { MenuBuilder.AddMenuEntry(FSplineComponentVisualizerCommands::Get().DeleteKey); MenuBuilder.AddMenuEntry(FSplineComponentVisualizerCommands::Get().DuplicateKey); MenuBuilder.AddSubMenu( LOCTEXT("SplinePointType", "Spline Point Type"), LOCTEXT("KeyTypeTooltip", "Define the type of the spline point."), FNewMenuDelegate::CreateSP(this, &FSplineComponentVisualizer::GenerateSplinePointTypeSubMenu)); // Only add the Automatic Tangents submenu if the key is a curve type USplineComponent* SplineComp = GetEditedSplineComponent(); if (SplineComp && SelectedKeyIndex < SplineComp->SplineInfo.Points.Num() && SplineComp->SplineInfo.Points[SelectedKeyIndex].IsCurveKey()) { MenuBuilder.AddSubMenu( LOCTEXT("ResetToAutomaticTangent", "Reset to Automatic Tangent"), LOCTEXT("ResetToAutomaticTangentTooltip", "Reset the spline point tangent to an automatically generated value."), FNewMenuDelegate::CreateSP(this, &FSplineComponentVisualizer::GenerateTangentTypeSubMenu)); } } } MenuBuilder.EndSection(); } TSharedPtr MenuWidget = MenuBuilder.MakeWidget(); return MenuWidget; } void FSplineComponentVisualizer::GenerateSplinePointTypeSubMenu(FMenuBuilder& MenuBuilder) const { MenuBuilder.AddMenuEntry(FSplineComponentVisualizerCommands::Get().SetKeyToCurve); MenuBuilder.AddMenuEntry(FSplineComponentVisualizerCommands::Get().SetKeyToLinear); MenuBuilder.AddMenuEntry(FSplineComponentVisualizerCommands::Get().SetKeyToConstant); } void FSplineComponentVisualizer::GenerateTangentTypeSubMenu(FMenuBuilder& MenuBuilder) const { MenuBuilder.AddMenuEntry(FSplineComponentVisualizerCommands::Get().ResetToUnclampedTangent); MenuBuilder.AddMenuEntry(FSplineComponentVisualizerCommands::Get().ResetToClampedTangent); } void FSplineComponentVisualizer::NotifyComponentModified() { USplineComponent* SplineComp = GetEditedSplineComponent(); SplineComp->UpdateSpline(); // Notify that the spline info has been modified UProperty* SplineInfoProperty = FindField(USplineComponent::StaticClass(), GET_MEMBER_NAME_CHECKED(USplineComponent, SplineInfo)); FPropertyChangedEvent PropertyChangedEvent(SplineInfoProperty); SplineComp->PostEditChangeProperty(PropertyChangedEvent); // Notify of change so any CS is re-run if (SplineOwningActor.IsValid()) { SplineOwningActor.Get()->PostEditMove(true); } GEditor->RedrawLevelEditingViewports(true); } #undef LOCTEXT_NAMESPACE