// Copyright Epic Games, Inc. All Rights Reserved. #include "BaseGizmos/GizmoElementGroup.h" #include "BaseGizmos/GizmoViewContext.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(GizmoElementGroup) void UGizmoElementGroupBase::ApplyUniformConstantScaleToTransform(double PixelToWorldScale, FTransform& InOutLocalToWorldTransform) const { return ApplyUniformConstantScaleToTransform(PixelToWorldScale, InOutLocalToWorldTransform, false); } void UGizmoElementGroupBase::ApplyUniformConstantScaleToTransform(double PixelToWorldScale, FTransform& InOutLocalToWorldTransform, const bool bInForceApply) const { double Scale = InOutLocalToWorldTransform.GetScale3D().X; if (bInForceApply || bConstantScale) { Scale *= PixelToWorldScale; } InOutLocalToWorldTransform.SetScale3D(FVector(Scale, Scale, Scale)); } void UGizmoElementGroupBase::Render(IToolsContextRenderAPI* RenderAPI, const FRenderTraversalState& RenderState) { FRenderTraversalState CurrentRenderState(RenderState); bool bVisibleViewDependent = UpdateRenderState(RenderAPI, FVector::ZeroVector, CurrentRenderState); if (bVisibleViewDependent) { ApplyUniformConstantScaleToTransform(CurrentRenderState.PixelToWorldScale, CurrentRenderState.LocalToWorldTransform); // Continue render even if not visible so all transforms will be cached // for subsequent line tracing. ForEachSubElement([RenderAPI, &CurrentRenderState](UGizmoElementBase* Element) { Element->Render(RenderAPI, CurrentRenderState); }); } } void UGizmoElementGroupBase::DrawHUD(FCanvas* Canvas, IToolsContextRenderAPI* RenderAPI, const FRenderTraversalState& RenderState) { FRenderTraversalState CurrentRenderState(RenderState); bool bVisibleViewDependent = UpdateRenderState(RenderAPI, FVector::ZeroVector, CurrentRenderState); if (bVisibleViewDependent) { ApplyUniformConstantScaleToTransform(CurrentRenderState.PixelToWorldScale, CurrentRenderState.LocalToWorldTransform); // Continue render even if not visible so all transforms will be cached // for subsequent line tracing. ForEachSubElement([Canvas, RenderAPI, &CurrentRenderState](UGizmoElementBase* Element) { Element->DrawHUD(Canvas, RenderAPI, CurrentRenderState); }); } } FInputRayHit UGizmoElementGroupBase::LineTrace(const UGizmoViewContext* ViewContext, const FLineTraceTraversalState& LineTraceState, const FVector& RayOrigin, const FVector& RayDirection, FLineTraceOutput& OutLineTraceOutput) { FInputRayHit Hit; FLineTraceTraversalState CurrentLineTraceState(LineTraceState); bool bHittableViewDependent = UpdateLineTraceState(ViewContext, FVector::ZeroVector, CurrentLineTraceState); if (bHittableViewDependent) { // Evaluates purely by distance (to the ray origin). struct FHitEvaluatorByDistance { /** Replaces the given Existing hit with the New hit if it's a better candidate. */ static void UpdateHit( const EGizmoElementHitSortType InSortType, FInputRayHit& OutTargetOrExistingHit, FLineTraceOutput& OutTargetOrExistingLineTraceOutput, const FInputRayHit& InNewHit, const FLineTraceOutput& InNewLineTraceOutput) { // Early-out if the new hit is not valid if (!InNewHit.bHit) { return; } const bool bIsExistingHitValid = OutTargetOrExistingHit.bHit; bool bUseNewHit = !bIsExistingHitValid; // Use the new hit by default if the existing hit is not valid // If the existing hit was valid, check if the new is better if (bIsExistingHitValid) { const bool bIsNewCloserHit = InNewHit.HitDepth < OutTargetOrExistingHit.HitDepth; bUseNewHit = bIsNewCloserHit; } if (bUseNewHit) { OutTargetOrExistingHit = InNewHit; OutTargetOrExistingLineTraceOutput = InNewLineTraceOutput; } } }; // Evaluates by priority, whether it's a surface hit (vs. within a tolerance), and by distance struct FHitEvaluatorByPriority { /** Replaces the given Existing hit with the New hit if it's a better candidate. */ static void UpdateHit( const EGizmoElementHitSortType InSortType, FInputRayHit& OutTargetOrExistingHit, FLineTraceOutput& OutTargetOrExistingLineTraceOutput, const FInputRayHit& InNewHit, const FLineTraceOutput& InNewLineTraceOutput) { // Early-out if the new hit is not valid if (!InNewHit.bHit) { return; } const bool bIsExistingHitValid = OutTargetOrExistingHit.bHit; bool bUseNewHit = !bIsExistingHitValid; // Use the new hit by default if the existing hit is not valid // If the existing hit was valid, check if the new is better if (bIsExistingHitValid) { const bool bTestPriority = EnumHasAnyFlags(InSortType, EGizmoElementHitSortType::Priority); const bool bTestSurface = EnumHasAnyFlags(InSortType, EGizmoElementHitSortType::Surface); const bool bTestClosest = EnumHasAnyFlags(InSortType, EGizmoElementHitSortType::Closest); const int32 ExistingPriority = OutTargetOrExistingLineTraceOutput.HitPriority; const int32 NewPriority = InNewLineTraceOutput.HitPriority; const bool bIsExistingSurfaceHit = OutTargetOrExistingLineTraceOutput.bIsSurfaceHit; const bool bIsNewSurfaceHit = InNewLineTraceOutput.bIsSurfaceHit || !bTestSurface; const double ExistingHitDepth = OutTargetOrExistingHit.HitDepth; const double NewHitDepth = InNewHit.HitDepth; const auto ShouldUseNewHit = [&]() -> bool { // Negative priorities are ALWAYS behind positive priorities, even if exact hits. if (bTestPriority && NewPriority < 0 && ExistingPriority >= 0) { return false; } // Prefer exact surface hits if (bTestSurface) { if (bIsNewSurfaceHit && !bIsExistingSurfaceHit) { // New is exact, existing wasn't, so use new return true; } if (!bIsNewSurfaceHit && bIsExistingSurfaceHit) { // Existing hit is exact, new isn't, so keep existing return false; } } // Higher priority win's (unless we already have an exact surface hit) if (bTestPriority && NewPriority > ExistingPriority) { return !(bTestSurface && bIsExistingSurfaceHit); } // Priorities are the same, so if we're testing closest, use that if (bTestClosest && (!bTestPriority || NewPriority == ExistingPriority)) { return NewHitDepth < ExistingHitDepth; } return false; }; bUseNewHit = ShouldUseNewHit(); } if (bUseNewHit) { OutTargetOrExistingHit = InNewHit; OutTargetOrExistingLineTraceOutput = InNewLineTraceOutput; } } }; ApplyUniformConstantScaleToTransform(CurrentLineTraceState.PixelToWorldScale, CurrentLineTraceState.LocalToWorldTransform); using FUpdateHitFunc = void(*)( const EGizmoElementHitSortType InSortType, FInputRayHit& OutTargetOrExistingHit, FLineTraceOutput& OutTargetOrExistingLineTraceOutput, const FInputRayHit& InNewHit, const FLineTraceOutput& InNewLineTraceOutput); FUpdateHitFunc UpdateHit = nullptr; if (CurrentLineTraceState.HitSortType == EGizmoElementHitSortType::Closest) { UpdateHit = FHitEvaluatorByDistance::UpdateHit; } else // @note: Add other sort types as needed { UpdateHit = FHitEvaluatorByPriority::UpdateHit; } if (ensure(UpdateHit)) { ForEachSubElement([&UpdateHit, ViewContext, &CurrentLineTraceState, &RayOrigin, &RayDirection, &Hit, &OutLineTraceOutput](UGizmoElementBase* Element) { FLineTraceOutput NewLineTraceOutput; const FInputRayHit NewHit = Element->LineTrace(ViewContext, CurrentLineTraceState, RayOrigin, RayDirection, NewLineTraceOutput); if (!NewHit.bHit) { return; } UpdateHit( CurrentLineTraceState.HitSortType, Hit, OutLineTraceOutput, NewHit, NewLineTraceOutput); }); if (bHitOwner && Hit.bHit) { Hit.SetHitObject(this); Hit.HitIdentifier = PartIdentifier; } } } return Hit; } void UGizmoElementGroupBase::SetPartIdentifier(uint32 InPartId, const bool bInOverrideUnsetChildren, const bool bInOverrideSet) { if (!bInOverrideUnsetChildren) { Super::SetPartIdentifier(InPartId, bInOverrideUnsetChildren); } // Only set if the part identifier is current Default (0), and bInOverrideSet is true if (bInOverrideSet || GetPartIdentifier() == 0) { UGizmoElementBase::SetPartIdentifier(InPartId); } ForEachSubElement([InPartId, bInOverrideUnsetChildren](UGizmoElementBase* Element) { // By default, bOverrideChildIfSet is true, but we only want that for the root element constexpr bool bOverrideChildIfSet = false; Element->SetPartIdentifier(InPartId, bInOverrideUnsetChildren, bOverrideChildIfSet); }); } UGizmoElementBase* UGizmoElementGroupBase::FindPartElement(const uint32 InPartId) { for (UGizmoElementBase* Element : Elements) { if (Element && Element->GetPartIdentifier() == InPartId) { return Element; } } return nullptr; } const UGizmoElementBase* UGizmoElementGroupBase::FindPartElement(const uint32 InPartId) const { for (UGizmoElementBase* Element : Elements) { if (Element && Element->GetPartIdentifier() == InPartId) { return Element; } } return nullptr; } void UGizmoElementGroupBase::Add(UGizmoElementBase* InElement) { if (!Elements.Contains(InElement)) { Elements.Add(InElement); } } void UGizmoElementGroupBase::Remove(UGizmoElementBase* InElement) { if (InElement) { int32 Index; if (Elements.Find(InElement, Index)) { Elements.RemoveAtSwap(Index); } } } TConstArrayView> UGizmoElementGroupBase::GetSubElements() const { return Elements; } bool UGizmoElementGroupBase::UpdatePartVisibleState(bool bVisible, uint32 InPartIdentifier, bool bInAllowMultipleElements) { bool bWasUpdated = false; if (Super::UpdatePartVisibleState(bVisible, InPartIdentifier, bInAllowMultipleElements)) { bWasUpdated = true; if (bWasUpdated && !bInAllowMultipleElements) { return true; } } for (UGizmoElementBase* Element : Elements) { if (Element) { bWasUpdated |= Element->UpdatePartVisibleState(bVisible, InPartIdentifier, bInAllowMultipleElements); if (bWasUpdated && !bInAllowMultipleElements) { return true; } } } return bWasUpdated; } TOptional UGizmoElementGroupBase::GetPartVisibleState(uint32 InPartIdentifier) const { TOptional Result = Super::GetPartVisibleState(InPartIdentifier); if (Result.IsSet()) { return Result; } for (UGizmoElementBase* Element : Elements) { if (Element) { Result = Element->GetPartVisibleState(InPartIdentifier); if (Result.IsSet()) { return Result; } } } return TOptional(); } bool UGizmoElementGroupBase::UpdatePartHittableState(bool bHittable, uint32 InPartIdentifier, bool bInAllowMultipleElements) { bool bWasUpdated = false; if (Super::UpdatePartHittableState(bHittable, InPartIdentifier, bInAllowMultipleElements)) { bWasUpdated = true; if (bWasUpdated && !bInAllowMultipleElements) { return true; } } for (UGizmoElementBase* Element : Elements) { if (Element) { bWasUpdated |= Element->UpdatePartHittableState(bHittable, InPartIdentifier, bInAllowMultipleElements); if (bWasUpdated && !bInAllowMultipleElements) { return true; } } } return bWasUpdated; } TOptional UGizmoElementGroupBase::GetPartHittableState(uint32 InPartIdentifier) const { TOptional Result = Super::GetPartHittableState(InPartIdentifier); if (Result.IsSet()) { return Result; } for (UGizmoElementBase* Element : Elements) { if (Element) { Result = Element->GetPartHittableState(InPartIdentifier); if (Result.IsSet()) { return Result; } } } return TOptional(); } bool UGizmoElementGroupBase::UpdatePartInteractionState(EGizmoElementInteractionState InInteractionState, uint32 InPartIdentifier, bool bInAllowMultipleElements) { bool bWasUpdated = false; if (Super::UpdatePartInteractionState(InInteractionState, InPartIdentifier, bInAllowMultipleElements)) { bWasUpdated = true; if (bWasUpdated && !bInAllowMultipleElements) { return true; } } for (UGizmoElementBase* Element : Elements) { if (Element) { bWasUpdated |= Element->UpdatePartInteractionState(InInteractionState, InPartIdentifier, bInAllowMultipleElements); if (bWasUpdated && !bInAllowMultipleElements) { return true; } } } return bWasUpdated; } TOptional UGizmoElementGroupBase::GetPartInteractionState(uint32 InPartIdentifier) const { TOptional Result = Super::GetPartInteractionState(InPartIdentifier); if (Result.IsSet()) { return Result; } for (UGizmoElementBase* Element : Elements) { if (Element) { Result = Element->GetPartInteractionState(InPartIdentifier); if (Result.IsSet()) { return Result; } } } return TOptional(); } void UGizmoElementGroupBase::SetConstantScale(bool bInConstantScale) { bConstantScale = bInConstantScale; } bool UGizmoElementGroupBase::GetConstantScale() const { return bConstantScale; } void UGizmoElementGroupBase::SetHitOwner(bool bInHitOwner) { bHitOwner = bInHitOwner; } bool UGizmoElementGroupBase::GetHitOwner() const { return bHitOwner; }