// Copyright Epic Games, Inc. All Rights Reserved. #include "BaseGizmos/IntervalGizmo.h" #include "InteractiveGizmoManager.h" #include "BaseGizmos/AxisPositionGizmo.h" #include "BaseGizmos/GizmoComponents.h" #include "BaseGizmos/GizmoLineHandleComponent.h" #include "BaseGizmos/GizmoViewContext.h" // need this to implement hover #include "BaseGizmos/GizmoBaseComponent.h" #include "Components/SphereComponent.h" #include "Components/PrimitiveComponent.h" #include "ContextObjectStore.h" #include "Engine/World.h" #include "Engine/CollisionProfile.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(IntervalGizmo) #define LOCTEXT_NAMESPACE "UIntervalGizmo" /** * FFloatParameterProxyChange tracks a change to the base transform for a FloatParameter */ class FFloatParameterProxyChange : public FToolCommandChange { public: FGizmoFloatParameterChange To; FGizmoFloatParameterChange From; virtual void Apply(UObject* Object) override { UGizmoLocalFloatParameterSource* ParameterSource = CastChecked(Object); ParameterSource->SetParameter(To.CurrentValue); } virtual void Revert(UObject* Object) override { UGizmoLocalFloatParameterSource* ParameterSource = CastChecked(Object); ParameterSource->SetParameter(From.CurrentValue); } virtual FString ToString() const override { return TEXT("FFloatParameterProxyChange"); } }; /** * FGizmoFloatParameterChangeSource generates FFloatParameterProxyChange instances on Begin/End. * Instances of this class can (for example) be attached to a UGizmoTransformChangeStateTarget for use TransformGizmo change tracking. */ class FGizmoFloatParameterChangeSource : public IToolCommandChangeSource { public: FGizmoFloatParameterChangeSource(UGizmoLocalFloatParameterSource* ProxyIn) { Proxy = ProxyIn; } virtual ~FGizmoFloatParameterChangeSource() {} TWeakObjectPtr Proxy; TUniquePtr ActiveChange; virtual void BeginChange() override { if (Proxy.IsValid()) { ActiveChange = MakeUnique(); ActiveChange->From = Proxy->LastChange; } } virtual TUniquePtr EndChange() override { if (Proxy.IsValid()) { ActiveChange->To = Proxy->LastChange; return MoveTemp(ActiveChange); } return TUniquePtr(); } virtual UObject* GetChangeTarget() override { return Proxy.Get(); } virtual FText GetChangeDescription() override { return LOCTEXT("FFGizmoFloatParameterChangeDescription", "GizmoFloatParameterChange"); } }; /** * This change source doesn't actually issue any valid transactions. Instead, it is a helper class * that can get attached to the interval gizmo's state target to fire off BeginEditSequence and * EndEditSequence on the start/end of a drag. */ class FIntervalGizmoChangeBroadcaster : public IToolCommandChangeSource { public: FIntervalGizmoChangeBroadcaster(UIntervalGizmo* IntervalGizmoIn) : IntervalGizmo(IntervalGizmoIn) {} virtual ~FIntervalGizmoChangeBroadcaster() {} TWeakObjectPtr IntervalGizmo; virtual void BeginChange() override { if (IntervalGizmo.IsValid()) { IntervalGizmo->BeginEditSequence(); } } virtual TUniquePtr EndChange() override { if (IntervalGizmo.IsValid()) { IntervalGizmo->EndEditSequence(); } return TUniquePtr(); } virtual UObject* GetChangeTarget() override { return IntervalGizmo.Get(); } virtual FText GetChangeDescription() override { return LOCTEXT("FIntervalGizmoChangeBroadcaster", "IntervalGizmoEdit"); } }; AIntervalGizmoActor::AIntervalGizmoActor() { // root component is a hidden sphere USphereComponent* SphereComponent = CreateDefaultSubobject(TEXT("GizmoCenter")); RootComponent = SphereComponent; SphereComponent->InitSphereRadius(1.0f); SphereComponent->SetVisibility(false); SphereComponent->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName); } AIntervalGizmoActor* AIntervalGizmoActor::ConstructDefaultIntervalGizmo(UWorld* World, UGizmoViewContext* GizmoViewContext) { FActorSpawnParameters SpawnInfo; AIntervalGizmoActor* NewActor = World->SpawnActor(FVector::ZeroVector, FRotator::ZeroRotator, SpawnInfo); const FLinearColor MintGreen(152 / 255.f, 1.f, 152 / 255.f); // add all possible interval components (note: some may be hidden / unused) NewActor->UpIntervalComponent = AddDefaultLineHandleComponent(World, NewActor, GizmoViewContext, MintGreen, FVector(0, 1, 0), FVector(0, 0, 1)); NewActor->DownIntervalComponent = AddDefaultLineHandleComponent(World, NewActor, GizmoViewContext, MintGreen, FVector(0, 1, 0), FVector(0, 0, 1)); NewActor->ForwardIntervalComponent = AddDefaultLineHandleComponent(World, NewActor, GizmoViewContext, MintGreen, FVector(1, 0, 0), FVector(0, 1, 0)); NewActor->BackwardIntervalComponent = AddDefaultLineHandleComponent(World, NewActor, GizmoViewContext, MintGreen, FVector(1, 0, 0), FVector(0, 1, 0)); NewActor->RightIntervalComponent = AddDefaultLineHandleComponent(World, NewActor, GizmoViewContext, MintGreen, FVector(0, 0, 1), FVector(1, 0, 0)); NewActor->LeftIntervalComponent = AddDefaultLineHandleComponent(World, NewActor, GizmoViewContext, MintGreen, FVector(0, 0, 1), FVector(1, 0, 0)); return NewActor; } UInteractiveGizmo* UIntervalGizmoBuilder::BuildGizmo(const FToolBuilderState& SceneState) const { UIntervalGizmo* NewGizmo = NewObject(SceneState.GizmoManager); NewGizmo->SetWorld(SceneState.World); UGizmoViewContext* GizmoViewContext = SceneState.ToolManager->GetContextObjectStore()->FindContext(); check(GizmoViewContext && GizmoViewContext->IsValidLowLevel()); // use default gizmo actor if client has not given us a new builder NewGizmo->SetGizmoActorBuilder(GizmoActorBuilder ? GizmoActorBuilder : MakeShared(GizmoViewContext)); // override default hover function if proposed if (UpdateHoverFunction) { NewGizmo->SetUpdateHoverFunction(UpdateHoverFunction); } if (UpdateCoordSystemFunction) { NewGizmo->SetUpdateCoordSystemFunction(UpdateCoordSystemFunction); } return NewGizmo; } // Init static FName FString UIntervalGizmo::GizmoName = TEXT("IntervalGizmo"); void UIntervalGizmo::SetWorld(UWorld* WorldIn) { this->World = WorldIn; } void UIntervalGizmo::SetGizmoActorBuilder(TSharedPtr Builder) { GizmoActorBuilder = Builder; } void UIntervalGizmo::SetUpdateHoverFunction(TFunction HoverFunction) { UpdateHoverFunction = HoverFunction; } void UIntervalGizmo::SetUpdateCoordSystemFunction(TFunction CoordSysFunction) { UpdateCoordSystemFunction = CoordSysFunction; } void UIntervalGizmo::SetWorldAlignmentFunctions(TUniqueFunction&& ShouldAlignDestinationIn, TUniqueFunction&& DestinationAlignmentRayCasterIn) { // Save these so that any later gizmo resets (using SetActiveTarget) keep the settings. ShouldAlignDestination = MoveTemp(ShouldAlignDestinationIn); DestinationAlignmentRayCaster = MoveTemp(DestinationAlignmentRayCasterIn); for (UInteractiveGizmo* SubGizmo : this->ActiveGizmos) { if (UAxisPositionGizmo* CastGizmo = Cast(SubGizmo)) { CastGizmo->ShouldUseCustomDestinationFunc = [this]() { return ShouldAlignDestination(); }; CastGizmo->CustomDestinationFunc = [this](const UAxisPositionGizmo::FCustomDestinationParams& Params, FVector& OutputPoint) { return DestinationAlignmentRayCaster(*Params.WorldRay, OutputPoint); }; CastGizmo->bCustomDestinationAlignsAxisOrigin = false; // We're aligning the endpoints of the intervals } } } void UIntervalGizmo::Setup() { UInteractiveGizmo::Setup(); UpdateHoverFunction = [](UPrimitiveComponent* Component, bool bHovering) { if (Cast(Component) != nullptr) { Cast(Component)->UpdateHoverState(bHovering); } }; UpdateCoordSystemFunction = [](UPrimitiveComponent* Component, EToolContextCoordinateSystem CoordSystem) { if (Cast(Component) != nullptr) { Cast(Component)->UpdateWorldLocalState(CoordSystem == EToolContextCoordinateSystem::World); } }; GizmoActor = GizmoActorBuilder->CreateNewGizmoActor(World); } void UIntervalGizmo::Shutdown() { ClearActiveTarget(); if (GizmoActor) { GizmoActor->Destroy(); GizmoActor = nullptr; } ClearSources(); } void UIntervalGizmo::Tick(float DeltaTime) { EToolContextCoordinateSystem CoordSystem = GetGizmoManager()->GetContextQueriesAPI()->GetCurrentCoordinateSystem(); check(CoordSystem == EToolContextCoordinateSystem::World || CoordSystem == EToolContextCoordinateSystem::Local) bool bUseLocalAxes = (GetGizmoManager()->GetContextQueriesAPI()->GetCurrentCoordinateSystem() == EToolContextCoordinateSystem::Local); // Update gizmo location. { USceneComponent* GizmoComponent = GizmoActor->GetRootComponent(); // move gizmo to target location FTransform TargetTransform = TransformProxy->GetTransform(); FVector SaveScale = TargetTransform.GetScale3D(); TargetTransform.SetScale3D(FVector(1, 1, 1)); GizmoComponent->SetWorldTransform(TargetTransform); } // Update the lengths EnumerateValidIntervals([this](UGizmoLocalFloatParameterSource* Source, UGizmoLineHandleComponent* Component, UGizmoComponentAxisSource* Axis, UE::Geometry::FInterval1f& IntervalRange, FVector3d Direction, float DirectionAxisSign) { if (Component) { Component->Length = Source->GetParameter(); } }); if (UpdateCoordSystemFunction) { for (UPrimitiveComponent* Component : ActiveComponents) { UpdateCoordSystemFunction(Component, CoordSystem); } } } void UIntervalGizmo::SetActiveTarget(UTransformProxy* TransformTargetIn, UGizmoLocalFloatParameterSource* UpInterval, UGizmoLocalFloatParameterSource* DownInterval, UGizmoLocalFloatParameterSource* ForwardInterval, IToolContextTransactionProvider* TransactionProvider) { FParameterSources Sources; Sources.UpInterval = UpInterval; Sources.DownInterval = DownInterval; Sources.ForwardInterval = ForwardInterval; SetActiveTarget(TransformTargetIn, Sources, TransactionProvider); } void UIntervalGizmo::SetActiveTarget(UTransformProxy* TransformTargetIn, const FParameterSources& ParameterSources, IToolContextTransactionProvider* TransactionProvider) { if (TransformProxy != nullptr) { ClearActiveTarget(); ClearSources(); } // This state target emits an explicit FChange that moves the GizmoActor root component during undo/redo. // It also opens/closes the Transaction that saves/restores the target object locations. if (TransactionProvider == nullptr) { TransactionProvider = GetGizmoManager(); } TransformProxy = TransformTargetIn; // parameters and init lengths for each interval UpIntervalSource = ParameterSources.UpInterval; DownIntervalSource = ParameterSources.DownInterval; ForwardIntervalSource = ParameterSources.ForwardInterval; BackwardIntervalSource = ParameterSources.BackwardInterval; RightIntervalSource = ParameterSources.RightInterval; LeftIntervalSource = ParameterSources.LeftInterval; if (ParameterSources.InitParameterRanges == EDefaultParameterRanges::HalfRange) { EnumerateAllIntervals([](UGizmoLocalFloatParameterSource* Source, UGizmoLocalFloatParameterSource* OppositeSource, UGizmoLineHandleComponent* Component, UE::Geometry::FInterval1f& IntervalRange, float DirectionAxisSign) { IntervalRange = DirectionAxisSign < 0 ? UE::Geometry::FInterval1f(-FLT_MAX, 0.f) : UE::Geometry::FInterval1f(0.f, FLT_MAX); } ); } else if (ParameterSources.InitParameterRanges == EDefaultParameterRanges::FullRange) { EnumerateAllIntervals([](UGizmoLocalFloatParameterSource* Source, UGizmoLocalFloatParameterSource* OppositeSource, UGizmoLineHandleComponent* Component, UE::Geometry::FInterval1f& IntervalRange, float DirectionAxisSign) { IntervalRange = UE::Geometry::FInterval1f(-FLT_MAX, FLT_MAX); } ); } else // EDefaultParameterRanges::HalfIfMatched { EnumerateAllIntervals([](UGizmoLocalFloatParameterSource* Source, UGizmoLocalFloatParameterSource* OppositeSource, UGizmoLineHandleComponent* Component, UE::Geometry::FInterval1f& IntervalRange, float DirectionAxisSign) { IntervalRange = UE::Geometry::FInterval1f(-FLT_MAX, FLT_MAX); if (OppositeSource) // if opposite interval exists, cut range in half { if (DirectionAxisSign < 0) { IntervalRange.Max = 0; } else { IntervalRange.Min = 0; } } } ); } // Get the parameter source to notify our delegate of any changes EnumerateValidIntervals([this](UGizmoLocalFloatParameterSource* Source, UGizmoLineHandleComponent* Component, UGizmoComponentAxisSource* Axis, UE::Geometry::FInterval1f& IntervalRange, FVector3d Direction, float DirectionAxisSign) { Source->OnParameterChanged.AddWeakLambda(this, [this, Direction, DirectionAxisSign](IGizmoFloatParameterSource*, FGizmoFloatParameterChange Change) { OnIntervalChanged.Broadcast(this, Direction, DirectionAxisSign * Change.CurrentValue); }); }); USceneComponent* GizmoComponent = GizmoActor->GetRootComponent(); // move gizmo to target location FTransform TargetTransform = TransformTargetIn->GetTransform(); FVector SaveScale = TargetTransform.GetScale3D(); TargetTransform.SetScale3D(FVector(1, 1, 1)); GizmoComponent->SetWorldTransform(TargetTransform); // TargetTransform tracks location of GizmoComponent. Note that TransformUpdated is not called during undo/redo transactions! // We currently rely on the transaction system to undo/redo target object locations. This will not work during runtime... GizmoComponent->TransformUpdated.AddLambda( [this, SaveScale](USceneComponent* Component, EUpdateTransformFlags /*UpdateTransformFlags*/, ETeleportType /*Teleport*/) { //this->GetGizmoManager()->DisplayMessage(TEXT("TRANSFORM UPDATED"), EToolMessageLevel::Internal); FTransform NewXForm = Component->GetComponentToWorld(); NewXForm.SetScale3D(SaveScale); this->TransformProxy->SetTransform(NewXForm); }); StateTarget = UGizmoTransformChangeStateTarget::Construct(GizmoComponent, LOCTEXT("UIntervalGizmoTransaction", "Interval"), TransactionProvider, this); StateTarget->DependentChangeSources.Add(MakeUnique(TransformProxy)); EnumerateValidIntervals([this](UGizmoLocalFloatParameterSource* Source, UGizmoLineHandleComponent* Component, UGizmoComponentAxisSource* Axis, UE::Geometry::FInterval1f& IntervalRange, FVector3d Direction, float DirectionAxisSign) { StateTarget->DependentChangeSources.Add(MakeUnique(Source)); }); // Have the state target notify us of the start/end of drags StateTarget->DependentChangeSources.Add(MakeUnique(this)); // root component provides local X/Y/Z axis, identified by AxisIndex AxisXSource = UGizmoComponentAxisSource::Construct(GizmoComponent, 0, true, this); AxisYSource = UGizmoComponentAxisSource::Construct(GizmoComponent, 1, true, this); AxisZSource = UGizmoComponentAxisSource::Construct(GizmoComponent, 2, true, this); EnumerateAllIntervals([](UGizmoLocalFloatParameterSource* Source, UGizmoLocalFloatParameterSource* OppositeSource, UGizmoLineHandleComponent* Component, UE::Geometry::FInterval1f& IntervalRange, float DirectionAxisSign) { Component->SetVisibility(false); }); EnumerateValidIntervals([this, GizmoComponent](UGizmoLocalFloatParameterSource* Source, UGizmoLineHandleComponent* Component, UGizmoComponentAxisSource* Axis, UE::Geometry::FInterval1f& IntervalRange, FVector3d Direction, float DirectionAxisSign) { AddIntervalHandleGizmo(GizmoComponent, Component, Axis, Source, IntervalRange.Min, IntervalRange.Max, StateTarget); ActiveComponents.Add(Component); Component->SetVisibility(true); }); } void UIntervalGizmo::SetVisibility(bool bVisible) { GizmoActor->SetActorHiddenInGame(bVisible == false); #if WITH_EDITOR GizmoActor->SetIsTemporarilyHiddenInEditor(bVisible == false); #endif } void UIntervalGizmo::ClearSources() { UpIntervalSource = nullptr; DownIntervalSource = nullptr; ForwardIntervalSource = nullptr; BackwardIntervalSource = nullptr; RightIntervalSource = nullptr; LeftIntervalSource = nullptr; } void UIntervalGizmo::ClearActiveTarget() { for (UInteractiveGizmo* Gizmo : ActiveGizmos) { GetGizmoManager()->DestroyGizmo(Gizmo); } ActiveGizmos.Empty(); ActiveComponents.Empty(); ClearSources(); TransformProxy = nullptr; } FTransform UIntervalGizmo::GetGizmoTransform() const { return TransformProxy->GetTransform(); } UInteractiveGizmo* UIntervalGizmo::AddIntervalHandleGizmo( USceneComponent* RootComponent, UPrimitiveComponent* HandleComponent, IGizmoAxisSource* AxisSource, IGizmoFloatParameterSource* FloatParameterSource, float MinParameter, float MaxParameter, IGizmoStateTarget* StateTargetIn) { // create axis-position gizmo, axis-position parameter will drive translation UAxisPositionGizmo* IntervalGizmo = Cast(GetGizmoManager()->CreateGizmo( UInteractiveGizmoManager::DefaultAxisPositionBuilderIdentifier)); check(IntervalGizmo); // axis source provides the scale axis IntervalGizmo->AxisSource = Cast(AxisSource); // parameter source maps axis-parameter-change to change in interval length IntervalGizmo->ParameterSource = UGizmoAxisIntervalParameterSource::Construct(FloatParameterSource, MinParameter, MaxParameter, this); // sub-component provides hit target UGizmoComponentHitTarget* HitTarget = UGizmoComponentHitTarget::Construct(HandleComponent, this); if (this->UpdateHoverFunction) { HitTarget->UpdateHoverFunction = [HandleComponent, this](bool bHovering) { this->UpdateHoverFunction(HandleComponent, bHovering); }; } IntervalGizmo->HitTarget = HitTarget; IntervalGizmo->StateTarget = Cast(StateTargetIn); IntervalGizmo->ShouldUseCustomDestinationFunc = [this]() { return ShouldAlignDestination(); }; IntervalGizmo->CustomDestinationFunc = [this](const UAxisPositionGizmo::FCustomDestinationParams& Params, FVector& OutputPoint) { return DestinationAlignmentRayCaster(*Params.WorldRay, OutputPoint); }; ActiveGizmos.Add(IntervalGizmo); return IntervalGizmo; } // Call IterFn on each Source/Component combination where the Source is not null. Note IterFn will still be called if the component and/or axis are null. void UIntervalGizmo::EnumerateValidIntervals( TFunctionRef IterFn ) { constexpr float IntervalMax = FLT_MAX; if (RightIntervalSource) { IterFn(RightIntervalSource, GizmoActor ? GizmoActor->RightIntervalComponent : nullptr, AxisXSource, RightIntervalRange, FVector3d(1, 0, 0), 1.f); } if (LeftIntervalSource) { IterFn(LeftIntervalSource, GizmoActor ? GizmoActor->LeftIntervalComponent : nullptr, AxisXSource, LeftIntervalRange, FVector3d(-1, 0, 0), -1.f); } if (ForwardIntervalSource) { IterFn(ForwardIntervalSource, GizmoActor ? GizmoActor->ForwardIntervalComponent : nullptr, AxisYSource, ForwardIntervalRange, FVector3d(0, 1, 0), 1.f); } if (BackwardIntervalSource) { IterFn(BackwardIntervalSource, GizmoActor ? GizmoActor->BackwardIntervalComponent : nullptr, AxisYSource, BackwardIntervalRange, FVector3d(0, -1, 0), -1.f); } if (UpIntervalSource) { IterFn(UpIntervalSource, GizmoActor ? GizmoActor->UpIntervalComponent : nullptr, AxisZSource, UpIntervalRange, FVector3d(0, 0, 1), 1.f); } if (DownIntervalSource) { IterFn(DownIntervalSource, GizmoActor ? GizmoActor->DownIntervalComponent : nullptr, AxisZSource, DownIntervalRange, FVector3d(0, 0, -1), -1.f); } } // Call IterFn on each Source/Component combination, including those where the Source is null void UIntervalGizmo::EnumerateAllIntervals(TFunctionRef IterFn) { if (GizmoActor) { IterFn(RightIntervalSource, LeftIntervalSource, GizmoActor->RightIntervalComponent, RightIntervalRange, 1.f); IterFn(LeftIntervalSource, RightIntervalSource, GizmoActor->LeftIntervalComponent, LeftIntervalRange, -1.f); IterFn(ForwardIntervalSource, BackwardIntervalSource, GizmoActor->ForwardIntervalComponent, ForwardIntervalRange, 1.f); IterFn(BackwardIntervalSource, ForwardIntervalSource, GizmoActor->BackwardIntervalComponent, BackwardIntervalRange, -1.f); IterFn(UpIntervalSource, DownIntervalSource, GizmoActor->UpIntervalComponent, UpIntervalRange, 1.f); IterFn(DownIntervalSource, UpIntervalSource, GizmoActor->DownIntervalComponent, DownIntervalRange, -1.f); } } float UGizmoAxisIntervalParameterSource::GetParameter() const { return FloatParameterSource->GetParameter(); } void UGizmoAxisIntervalParameterSource::SetParameter(float NewValue) { NewValue = FMath::Clamp(NewValue, MinParameter, MaxParameter); FloatParameterSource->SetParameter(NewValue); } void UGizmoAxisIntervalParameterSource::BeginModify() { FloatParameterSource->BeginModify(); } void UGizmoAxisIntervalParameterSource::EndModify() { FloatParameterSource->EndModify(); } UGizmoAxisIntervalParameterSource* UGizmoAxisIntervalParameterSource::Construct( IGizmoFloatParameterSource* FloatSourceIn, float ParameterMin, float ParameterMax, UObject* Outer) { UGizmoAxisIntervalParameterSource* NewSource = NewObject(Outer); NewSource->FloatParameterSource = Cast(FloatSourceIn); // Clamp the initial value float DefaultValue = NewSource->FloatParameterSource->GetParameter(); DefaultValue = FMath::Clamp(DefaultValue, ParameterMin, ParameterMax); NewSource->FloatParameterSource->SetParameter(DefaultValue); // record the min / max allowed NewSource->MinParameter = ParameterMin; NewSource->MaxParameter = ParameterMax; return NewSource; } #undef LOCTEXT_NAMESPACE