Files
2025-05-18 13:04:45 +08:00

137 lines
5.1 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "BaseBehaviors/BehaviorTargetInterfaces.h"
#include "InteractiveGizmo.h"
#include "Math/MathFwd.h"
#include "FreeRotationSubGizmo.generated.h"
class IGizmoAxisSource;
class IGizmoClickTarget;
class IGizmoStateTarget;
class IGizmoTransformSource;
class UClickDragInputBehavior;
class UGizmoViewContext;
namespace UE::GizmoUtil
{
struct FTransformSubGizmoCommonParams;
struct FTransformSubGizmoSharedState;
}
/**
* A free rotation sub gizmo implements an arcball-like rotation.
*/
UCLASS(MinimalAPI)
class UFreeRotationSubGizmo : public UInteractiveGizmo, public IClickDragBehaviorTarget, public IHoverBehaviorTarget
{
GENERATED_BODY()
public:
/**
* Initializes the properties for the gizmo
*/
bool InitializeAsRotationGizmo(
const UE::GizmoUtil::FTransformSubGizmoCommonParams& InitializationParams,
UGizmoViewContext* ViewContext,
UE::GizmoUtil::FTransformSubGizmoSharedState* SharedState);
// There are many potential approaches to doing a free rotate handle. Currently we
// implement IncrementalSphereBound from the below, which feels pretty intuitive,
// but we could add the other ones if desired.
//enum class EMode
//{
// // Intersect rays with a sphere and rotate the latest result such that the previous
// // intersection goes to the latest intersection. Outside of sphere, assume you are
// // on the tangent side of the sphere.
// // This is similar to how the normal editor gizmo works if you enable arcball rotation.
// IncrementalSphereBound,
//
// // Like IncrementalSphereBound, but rotate the original (not latest) transform such
// // that the first (i.e. original) intersection goes to latest intersection.
// // This is similar to how the ball works in the new gizmos (i.e. if Enable New Gizmos) is true.
// SourceToDestSphereBound,
// // Similar to SourceToDestSphereBound in that we're determining our rotation axis based on
// // the first intersection going to latest intersection, but the amount to rotate is determined
// // by the distance in the camera plane, so that we can keep rotating the object 360 degrees or
// // more with one drag.
// SourceToDestUnbounded
//};
//void SetMode(EMode::ModeIn);
/**
* Determines the size of the invisible sphere we raycast to perform the rotation.
*/
void SetUnscaledSphereRadius(double Radius) { UnscaledSphereRadius = Radius; }
/**
* When true (default) a circle is drawn to show the outside bounds of the interaction sphere while
* interacting with the gizmo.
*/
void SetShowSphereBoundsDuringInteraction(bool bOn) { bShowSphereBoundsDuringInteraction = bOn; }
// UInteractiveGizmo overrides
virtual void Setup() override;
virtual void Render(IToolsContextRenderAPI* RenderAPI) override;
virtual void Tick(float DeltaTime) override;
// IClickDragBehaviorTarget implementation
virtual FInputRayHit CanBeginClickDragSequence(const FInputDeviceRay& PressPos) override;
virtual void OnClickPress(const FInputDeviceRay& PressPos) override;
virtual void OnClickDrag(const FInputDeviceRay& DragPos) override;
virtual void OnClickRelease(const FInputDeviceRay& ReleasePos) override;
virtual void OnTerminateDragSequence() override;
// IHoverBehaviorTarget implementation
virtual FInputRayHit BeginHoverSequenceHitTest(const FInputDeviceRay& PressPos) override;
virtual void OnBeginHover(const FInputDeviceRay& DevicePos) override;
virtual bool OnUpdateHover(const FInputDeviceRay& DevicePos) override;
virtual void OnEndHover() override;
public:
// The below properties can be manipulated for more fine-grained control, but typically it is sufficient
// to use the initialization method above.
/** AxisSource provides the origin of the interaction sphere and the plane to raycast when hitting outside the sphere */
UPROPERTY()
TScriptInterface<IGizmoAxisSource> AxisSource;
/** The HitTarget provides a hit-test against some 3D element (presumably a visual widget) that controls when interaction can start */
UPROPERTY()
TScriptInterface<IGizmoClickTarget> HitTarget;
/** StateTarget is notified when interaction starts and ends, so that things like undo/redo can be handled externally */
UPROPERTY()
TScriptInterface<IGizmoStateTarget> StateTarget;
/** Target that is rotated by the sub gizmo. */
UPROPERTY()
TScriptInterface<IGizmoTransformSource> TransformSource;
/** View info used during raycasts */
UPROPERTY()
TObjectPtr<UGizmoViewContext> GizmoViewContext = nullptr;
/** The mouse click behavior of the gizmo is accessible so that it can be modified to use different mouse keys. */
UPROPERTY()
TObjectPtr<UClickDragInputBehavior> MouseBehavior;
private:
bool bInInteraction = false;
bool bShowSphereBoundsDuringInteraction = true;
FVector LastSphereIntersectionPoint;
double InteractionSphereRadius = 0;
bool ClickPress_IncrementalSphereBound(const FInputDeviceRay& PressPos);
bool ClickDrag_IncrementalSphereBound(const FInputDeviceRay& DragPos);
double UnscaledSphereRadius = 100;
// Helper that can hold some extra upkeep to do during Tick (used to
// update a camera axis source if needed).
TUniqueFunction<void(float DeltaTime)> CustomTickFunction;
};