From aed58897cf814c52336e07b670753c04be9aebf9 Mon Sep 17 00:00:00 2001 From: DuOtto Date: Wed, 15 Jul 2026 22:09:56 +0200 Subject: [PATCH] Added Gesture Actions, Gesture Definitions, TableIcons, ... --- Table/.vsconfig | 2 - Table/Automation_Table.slnx | 65 ++++ .../GestureActionDataTable.uasset | Bin 0 -> 2286 bytes .../GestureDefinitionDataTable.uasset | Bin 0 -> 2603 bytes Table/Content/WorldObjects/GameLevel.umap | Bin 25406 -> 25874 bytes Table/Source/Table.Target.cs | 4 +- Table/Source/Table/Private/GestureAction.cpp | 40 ++- .../Table/Private/GestureDefinition.cpp | 101 ++++-- .../Table/Private/GestureManagerComponent.cpp | 322 +++++++++++------- Table/Source/Table/Private/HandActor.cpp | 16 +- Table/Source/Table/Private/HandManager.cpp | 6 +- .../Source/Table/Private/MyGestureActions.cpp | 83 +++++ .../Table/Private/MyTableIconActors.cpp | 167 +++++++++ Table/Source/Table/Private/TableIconActor.cpp | 30 ++ Table/Source/Table/Private/UserHUDWidget.cpp | 4 +- Table/Source/Table/Public/GestureAction.h | 44 ++- Table/Source/Table/Public/GestureData.h | 14 +- Table/Source/Table/Public/GestureDefinition.h | 88 ++++- .../Table/Public/GestureManagerComponent.h | 24 +- Table/Source/Table/Public/HandActor.h | 18 +- Table/Source/Table/Public/HandManager.h | 7 +- Table/Source/Table/Public/MyGestureActions.h | 36 ++ Table/Source/Table/Public/MyTableIconActors.h | 93 +++++ Table/Source/Table/Public/TableIconActor.h | 36 ++ Table/Source/Table/Table.Build.cs | 3 +- Table/Source/TableEditor.Target.cs | 4 +- Table/Table.slnx | 44 +++ Table/Table.uproject | 4 +- 28 files changed, 1041 insertions(+), 214 deletions(-) create mode 100644 Table/Automation_Table.slnx create mode 100644 Table/Content/GeneralStuff/GestureActionDataTable.uasset create mode 100644 Table/Content/GeneralStuff/GestureDefinitionDataTable.uasset create mode 100644 Table/Source/Table/Private/MyGestureActions.cpp create mode 100644 Table/Source/Table/Private/MyTableIconActors.cpp create mode 100644 Table/Source/Table/Private/TableIconActor.cpp create mode 100644 Table/Source/Table/Public/MyGestureActions.h create mode 100644 Table/Source/Table/Public/MyTableIconActors.h create mode 100644 Table/Source/Table/Public/TableIconActor.h create mode 100644 Table/Table.slnx diff --git a/Table/.vsconfig b/Table/.vsconfig index b981b2e..93c1dfa 100644 --- a/Table/.vsconfig +++ b/Table/.vsconfig @@ -4,8 +4,6 @@ "Component.Unreal.Debugger", "Component.Unreal.Ide", "Microsoft.Net.Component.4.6.2.TargetingPack", - "Microsoft.VisualStudio.Component.VC.14.38.17.8.ATL", - "Microsoft.VisualStudio.Component.VC.14.38.17.8.x86.x64", "Microsoft.VisualStudio.Component.VC.14.44.17.14.ATL", "Microsoft.VisualStudio.Component.VC.14.44.17.14.x86.x64", "Microsoft.VisualStudio.Component.VC.Llvm.Clang", diff --git a/Table/Automation_Table.slnx b/Table/Automation_Table.slnx new file mode 100644 index 0000000..bcfaa3a --- /dev/null +++ b/Table/Automation_Table.slnx @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Table/Content/GeneralStuff/GestureActionDataTable.uasset b/Table/Content/GeneralStuff/GestureActionDataTable.uasset new file mode 100644 index 0000000000000000000000000000000000000000..e7ef1e6c91588fd07e59286c4e58644d7ad37438 GIT binary patch literal 2286 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 J7vAd+{0A&1)Q|uG diff --git a/Table/Source/Table.Target.cs b/Table/Source/Table.Target.cs index 0c5adfa..2ca872f 100644 --- a/Table/Source/Table.Target.cs +++ b/Table/Source/Table.Target.cs @@ -8,8 +8,8 @@ public class TableTarget : TargetRules public TableTarget(TargetInfo Target) : base(Target) { Type = TargetType.Game; - DefaultBuildSettings = BuildSettingsVersion.V6; - IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_7; + DefaultBuildSettings = BuildSettingsVersion.V7; + IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_8; ExtraModuleNames.Add("Table"); } } diff --git a/Table/Source/Table/Private/GestureAction.cpp b/Table/Source/Table/Private/GestureAction.cpp index 5cf6af8..5aa93a4 100644 --- a/Table/Source/Table/Private/GestureAction.cpp +++ b/Table/Source/Table/Private/GestureAction.cpp @@ -3,44 +3,62 @@ #include "GestureAction.h" #include "HandActor.h" +#include "TableIconActor.h" // Sets default values AGestureAction::AGestureAction() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. - PrimaryActorTick.bCanEverTick = true; - + PrimaryActorTick.bCanEverTick = false; } // Called when the game starts or when spawned void AGestureAction::BeginPlay() { Super::BeginPlay(); - } -// Called every frame -void AGestureAction::Tick(float DeltaTime) +void AGestureAction::InitAction_Implementation(AHandActor* Hand, const FGestureBinding& NewBinding, float NewUEUnitsPerUnit) { - Super::Tick(DeltaTime); - + HandA = Hand; + bRequiresSecondHand = Binding.LeftGesture != "" && Binding.RightGesture != ""; + bExtendable = Binding.ExtendedGestures.IsEmpty(); + Binding = NewBinding; + HandManager = Hand->HandManager; + UEUnitsPerUnit = NewUEUnitsPerUnit; } -void AGestureAction::UpdateAction() +void AGestureAction::UpdateAction_Implementation() { Destroy(); } -void AGestureAction::AbortAction() +void AGestureAction::AbortAction_Implementation() { + for (ATableIconActor* Icon : Icons) + Icon->bPersistOnActionEnd = false; + EndGesture(); } -void AGestureAction::AddExtension(AHandActor* OtherHand) +void AGestureAction::AddExtension_Implementation(AGestureAction* OtherAction) +{ + LinkedActions = OtherAction; +} + +void AGestureAction::AddOtherHand_Implementation(AHandActor* OtherHand) { HandB = OtherHand; } -void AGestureAction::EndGesture() +void AGestureAction::EndGesture_Implementation() { + DestroyIcons(); Destroy(); +} + +void AGestureAction::DestroyIcons() +{ + for (ATableIconActor* Icon : Icons) + if (IsValid(Icon) ) Icon->OnActionEnded(); + Icons.Empty(); } \ No newline at end of file diff --git a/Table/Source/Table/Private/GestureDefinition.cpp b/Table/Source/Table/Private/GestureDefinition.cpp index e4275f7..069d1b9 100644 --- a/Table/Source/Table/Private/GestureDefinition.cpp +++ b/Table/Source/Table/Private/GestureDefinition.cpp @@ -40,6 +40,11 @@ const FVector& UGestureDefinition::GetPoint(const AHandActor* Hand, int32 Index) return Hand->Points[Index]; } +bool UGestureDefinition::IsFingerExtended(const AHandActor* Hand, EHandFinger Finger, float Threshold) const +{ + return GetFingerExtendedScore(Hand, Finger) > Threshold; +} + float UGestureDefinition::GetHandScale(const AHandActor* Hand) const { const TArray& P = Hand->Points; @@ -53,30 +58,44 @@ float UGestureDefinition::GetNormalizedDistance(const AHandActor* Hand, int32 A, } float UGestureDefinition::GetPinchScore(const AHandActor* Hand, EHandFinger A, EHandFinger B) const { - float Dist = GetNormalizedDistance( - Hand, - GetFingerTipIndex(A), - GetFingerTipIndex(B) - ); + const FVector& P1 = Hand->Points[GetFingerTipIndex(A)]; + const FVector& P2 = Hand->Points[GetFingerTipIndex(B)]; + + float Dist = FVector::Dist(P1, P2); + + float HandScale = FVector::Dist( Hand->Points[0], Hand->Points[9] ); + + float Normalized = Dist / HandScale; + + //UE_LOG(LogTemp, Warning, TEXT("Dist: %.3f | HandScale: %.3f | Norm: %.3f"), Dist, HandScale, Normalized); const float Threshold = 0.25f; - return 1.0f - FMath::Clamp(Dist / Threshold, 0.f, 1.f); + return 1.f - FMath::Clamp( Normalized / Threshold, 0.f, 1.f ); } float UGestureDefinition::GetFingerExtendedScore(const AHandActor* Hand, EHandFinger Finger) const { const TArray& P = Hand->Points; + if (Finger == EHandFinger::Thumb) + { + // Thumb: compare tip->base against index base->wrist as reference direction + const FVector& Base = P[2]; + const FVector& Tip = P[4]; + const FVector& Ref = P[5]; // index base as stable reference + float Dot = FVector::DotProduct( + (Tip - Base).GetSafeNormal(), + (Ref - P[0]).GetSafeNormal()); + return FMath::Clamp(Dot, 0.f, 1.f); + } + const FVector& Wrist = P[0]; const FVector& Base = P[GetFingerBaseIndex(Finger)]; const FVector& Tip = P[GetFingerTipIndex(Finger)]; - float Dot = FVector::DotProduct( (Tip - Base).GetSafeNormal(), - (Base - Wrist).GetSafeNormal() - ); - + (Base - Wrist).GetSafeNormal()); return FMath::Clamp(Dot, 0.f, 1.f); } @@ -91,9 +110,7 @@ float UGestureDefinition::GetFingersAverage(const AHandActor* Hand, const TArray for (EHandFinger Finger : Fingers) { - Sum += bExtended - ? GetFingerExtendedScore(Hand, Finger) - : GetFingerCurledScore(Hand, Finger); + Sum += bExtended ? GetFingerExtendedScore(Hand, Finger) : GetFingerCurledScore(Hand, Finger); } return Fingers.Num() > 0 ? Sum / Fingers.Num() : 0.f; @@ -107,19 +124,57 @@ FVector UGestureDefinition::GetFingerDirection(const AHandActor* Hand, EHandFing return (Tip - Base).GetSafeNormal(); } -float UGesture_PinchPoint::Evaluate(AHandActor* Hand) const +float UGesture_PinchIndex::Evaluate(AHandActor* Hand) const { float Pinch = GetPinchScore(Hand, EHandFinger::Thumb, EHandFinger::Index); float IndexExtended = GetFingerExtendedScore(Hand, EHandFinger::Index); - float OthersCurled = GetFingersAverage( - Hand, - { EHandFinger::Middle, EHandFinger::Ring, EHandFinger::Pinky }, - false - ); + float OthersCurled = GetFingersAverage( Hand, { EHandFinger::Middle, EHandFinger::Ring, EHandFinger::Pinky }, false ); - return - Pinch * 0.5f + - IndexExtended * 0.3f + - OthersCurled * 0.2f; + return Pinch * 0.5f + IndexExtended * 0.3f + OthersCurled * 0.2f; +} + +float UGesture_PinchMiddleIndex::Evaluate(AHandActor* Hand) const +{ + float Pinch = GetPinchScore(Hand, EHandFinger::Thumb, EHandFinger::Middle); + Pinch += GetPinchScore(Hand, EHandFinger::Thumb, EHandFinger::Index); + + float MiddleExtended = GetFingerExtendedScore(Hand, EHandFinger::Middle); + float OthersCurled = GetFingersAverage( Hand, { EHandFinger::Ring, EHandFinger::Pinky }, false ); + + return Pinch * 0.5f + MiddleExtended * 0.3f + OthersCurled * 0.2f; + +} + +float UGesture_PointIndex::Evaluate(AHandActor* Hand) const +{ + float Index = GetFingerExtendedScore(Hand, EHandFinger::Index); + float Middle = GetFingerCurledScore(Hand, EHandFinger::Middle); + float Ring = GetFingerCurledScore(Hand, EHandFinger::Ring); + float Pinky = GetFingerCurledScore(Hand, EHandFinger::Pinky); + float Thumb = GetFingerCurledScore(Hand, EHandFinger::Thumb); + + return ( Index + Middle + Ring + Pinky + Thumb ) / 5.f; +} + +float UGesture_PointMiddle::Evaluate(AHandActor* Hand) const +{ + float Index = GetFingerCurledScore(Hand, EHandFinger::Index); + float Middle = GetFingerExtendedScore(Hand, EHandFinger::Middle); + float Ring = GetFingerCurledScore(Hand, EHandFinger::Ring); + float Pinky = GetFingerCurledScore(Hand, EHandFinger::Pinky); + float Thumb = GetFingerCurledScore(Hand, EHandFinger::Thumb); + + return (Index + Middle + Ring + Pinky + Thumb) / 5.f; +} + +float UGesture_PointIndexMiddle::Evaluate(AHandActor* Hand) const +{ + float Index = GetFingerExtendedScore(Hand, EHandFinger::Index); + float Middle = GetFingerExtendedScore(Hand, EHandFinger::Middle); + float Ring = GetFingerCurledScore(Hand, EHandFinger::Ring); + float Pinky = GetFingerCurledScore(Hand, EHandFinger::Pinky); + float Thumb = GetFingerCurledScore(Hand, EHandFinger::Thumb); + + return (Index + Middle + Ring + Pinky + Thumb) / 5.f; } diff --git a/Table/Source/Table/Private/GestureManagerComponent.cpp b/Table/Source/Table/Private/GestureManagerComponent.cpp index 8fe1a36..9fbc670 100644 --- a/Table/Source/Table/Private/GestureManagerComponent.cpp +++ b/Table/Source/Table/Private/GestureManagerComponent.cpp @@ -14,7 +14,7 @@ UGestureManagerComponent::UGestureManagerComponent() // off to improve performance if you don't need them. PrimaryComponentTick.bCanEverTick = false; - HandManager = Cast(GetOwner()); + } @@ -23,6 +23,8 @@ void UGestureManagerComponent::BeginPlay() { Super::BeginPlay(); + HandManager = Cast(GetOwner()); + HandManager->OnHandAdded.AddDynamic(this, &UGestureManagerComponent::OnHandAdded); HandManager->OnHandRemoved.AddDynamic(this, &UGestureManagerComponent::OnHandRemoved); @@ -42,17 +44,16 @@ void UGestureManagerComponent::EvaluateHands() if (Hand->bUsedThisTick) continue; switch (Hand->Phase) { - case EGesturePhase::Started: + case EHandGesturePhase::Started: TriggerAction(Hand); break; - case EGesturePhase::Ended: + case EHandGesturePhase::Ended: Hand->ResetGesture(); - EndGesture(); break; } } - UpdateOngoingAction(); + UpdateOngoingActions(); } void UGestureManagerComponent::OnHandAdded(AHandActor* Hand) @@ -62,14 +63,16 @@ void UGestureManagerComponent::OnHandAdded(AHandActor* Hand) void UGestureManagerComponent::OnHandRemoved(AHandActor* Hand) { - Hand->PairedHand->PairedHand = nullptr; - Hand->ResetGesture(); + if (!Hand->ActiveAction) return; + if (!ActiveGestureActions.Contains(Hand->ActiveAction)) return; + Hand->ActiveAction->Destroy(); + ActiveGestureActions.Remove(Hand->ActiveAction); } void UGestureManagerComponent::LoadBindings(const FString& FilePath) { - FString JsonString; - + FString JsonString = ""; + FString Temp = ""; if (!FFileHelper::LoadFileToString(JsonString, *FilePath)) { return; @@ -92,9 +95,12 @@ void UGestureManagerComponent::LoadBindings(const FString& FilePath) FGestureBinding Binding; - Obj->TryGetStringField(TEXT("action"), Binding.ActionName); - Obj->TryGetStringField(TEXT("left"), Binding.LeftGesture); - Obj->TryGetStringField(TEXT("right"), Binding.RightGesture); + Obj->TryGetStringField(TEXT("action"), Temp); + Binding.ActionName = FName(Temp); + Obj->TryGetStringField(TEXT("left"), Temp); + Binding.LeftGesture = FName(Temp); + Obj->TryGetStringField(TEXT("right"), Temp); + Binding.RightGesture = FName(Temp); const TArray>* ExtArray; if (Obj->TryGetArrayField(TEXT("extensions"), ExtArray)) @@ -106,10 +112,12 @@ void UGestureManagerComponent::LoadBindings(const FString& FilePath) FExtendedGestureBinding Ext; - ExtObj->TryGetStringField(TEXT("gesture"), Ext.GestureName); - ExtObj->TryGetStringField(TEXT("action"), Ext.GestureAction); + ExtObj->TryGetStringField(TEXT("gesture"), Temp); + Ext.GestureName = FName(Temp); + ExtObj->TryGetStringField(TEXT("action"), Temp); + Ext.GestureAction = FName(Temp); - Binding.ExentdedGestures.Add(Ext); + Binding.ExtendedGestures.Add(Ext); } } @@ -143,6 +151,7 @@ void UGestureManagerComponent::BuildDefinitionMap() UGestureDefinition* Def = NewObject(this, Row->DefinitionClass); DefinitionMap.Add(Def->GestureName, Def); + GestureDefinitions.Add(Def); } } @@ -159,176 +168,171 @@ void UGestureManagerComponent::BuildActionMap() { if (!Row || !Row->ActionClass) continue; - ActionClassMap.Add( - Row->ActionClass->GetName(), - Row->ActionClass - - ); + ActionClassMap.Add( FName(Row->ActionClass->GetName()), Row->ActionClass ); } } void UGestureManagerComponent::CheckGestureDefinitions(TArray& Hands) { - const float MinScoreThreshold = 0.5f; + //const float MinScoreThreshold = 0.7f; + for (AHandActor* Hand : Hands) { + float Threshold = (Hand->Phase == EHandGesturePhase::Ongoing || Hand->Phase == EHandGesturePhase::Started) ? 0.60f : 0.75f; + float BestScore = 0.f; - FString BestGesture = ""; + FName BestGesture = ""; for (UGestureDefinition* Gesture : GestureDefinitions) { - float Score = Gesture->Evaluate(Hand); + float Score = Gesture->Evaluate(Hand); // Getting the Score of the gestures to check - if (Score > BestScore) + //UE_LOG(LogTemp, Warning, TEXT("%s -> %s : %.3f"), *Hand->GetName(), *Gesture->GestureName, Score); + + if (Score > BestScore) // Getting the gesture with the highest Score { BestScore = Score; BestGesture = Gesture->GestureName; } } - if (BestScore < MinScoreThreshold) + // Check if the gesture with the highest score is a valid Gesture + if (BestScore < Threshold) { - if (Hand->Phase != EGesturePhase::None) + if (Hand->Phase != EHandGesturePhase::None) { Hand->FailFrames++; - + UE_LOG(LogTemp, Warning, + TEXT("[%s] Below threshold (%s) Score=%.3f Fail=%d/%d"), + *Hand->GetName(), + *Hand->ActiveGestureName.ToString(), + BestScore, + Hand->FailFrames, + FailThreshold); + //if (Hand->FailFrames >= FailThreshold) Hand->Phase = EHandGesturePhase::Ended; if (Hand->FailFrames >= FailThreshold) { - Hand->Phase = EGesturePhase::Ended; + UE_LOG(LogTemp, Warning, + TEXT("[%s] -> ENDED (%s)"), + *Hand->GetName(), + *Hand->ActiveGestureName.ToString()); + + Hand->Phase = EHandGesturePhase::Ended; } } - continue; } + // Handle valid Gesture switch (Hand->Phase) { - case EGesturePhase::None: + case EHandGesturePhase::None: // Starting a Candidate Hand->ActiveGestureName = BestGesture; - Hand->Phase = EGesturePhase::Candidate; + Hand->Phase = EHandGesturePhase::Candidate; Hand->CandidateFrames = 1; Hand->FailFrames = 0; + UE_LOG(LogTemp, Warning, + TEXT("[%s] NONE -> CANDIDATE (%s)"), + *Hand->GetName(), + *BestGesture.ToString()); break; - case EGesturePhase::Candidate: - if (Hand->ActiveGestureName != BestGesture) + case EHandGesturePhase::Candidate: // Checking if the Candidate stays valid or another gesture is made + if (Hand->ActiveGestureName == BestGesture) { - Hand->FailFrames++; - break; + Hand->CandidateFrames++; + Hand->FailFrames = 0; + + UE_LOG(LogTemp, Warning, + TEXT("[%s] Candidate %s (%d/%d)"), + *Hand->GetName(), + *BestGesture.ToString(), + Hand->CandidateFrames, + CandidateThreshold); + + //if (Hand->CandidateFrames >= CandidateThreshold) Hand->Phase = EHandGesturePhase::Started; + if (Hand->CandidateFrames >= CandidateThreshold) + { + Hand->Phase = EHandGesturePhase::Started; + + UE_LOG(LogTemp, Warning, + TEXT("[%s] CANDIDATE -> STARTED (%s)"), + *Hand->GetName(), + *BestGesture.ToString()); + } + break; } + UE_LOG(LogTemp, Warning, + TEXT("[%s] Candidate switched %s -> %s"), + *Hand->GetName(), + *Hand->ActiveGestureName.ToString(), + *BestGesture.ToString()); - Hand->CandidateFrames++; + Hand->ActiveGestureName = BestGesture; + Hand->CandidateFrames = 1; Hand->FailFrames = 0; - - if (Hand->CandidateFrames >= CandidateThreshold) - Hand->Phase = EGesturePhase::Started; - break; - case EGesturePhase::Ongoing: + case EHandGesturePhase::Ongoing: // Checking if the gesture is still ongoing + //if (Hand->ActiveGestureName != BestGesture) Hand->FailFrames++; + //else Hand->FailFrames = 0; if (Hand->ActiveGestureName != BestGesture) { Hand->FailFrames++; - if (Hand->FailFrames >= FailThreshold) - { - Hand->Phase = EGesturePhase::Ended; - } + UE_LOG(LogTemp, Warning, + TEXT("[%s] Ongoing fail %s (%d/%d)"), + *Hand->GetName(), + *Hand->ActiveGestureName.ToString(), + Hand->FailFrames, + FailThreshold); } else { - Hand->FailFrames = 0; + if (Hand->FailFrames > 0) + { + UE_LOG(LogTemp, Warning, + TEXT("[%s] Ongoing recovered %s"), + *Hand->GetName(), + *Hand->ActiveGestureName.ToString()); + } } break; + //case EHandGesturePhase::Started: + // break; + default: // If the gesture would be stuck somewhere + if (Hand->ActiveGestureName != BestGesture) Hand->FailFrames++; + + if (Hand->FailFrames >= FailThreshold) Hand->ResetGesture(); // This optimaly shouldnt be called here + break; } - - - if (Hand->FailFrames >= FailThreshold) - Hand->Phase = EGesturePhase::Ended; + // Checking if a gesture has ended + if (Hand->FailFrames >= FailThreshold) Hand->Phase = EHandGesturePhase::Ended; } } void UGestureManagerComponent::TriggerAction(AHandActor* Hand) { - if (!Hand->bUsedThisTick) return; - - if (Hand->IsPaired()) - { - AHandActor* Other = Hand->PairedHand; - - for (const FGestureBinding& Binding : GestureBindings) - { - const bool bLeftMatch = (Hand->Handedness == EHandedness::Left && Hand->ActiveGestureName == Binding.LeftGesture && - Other->ActiveGestureName == Binding.RightGesture); - - const bool bRightMatch = (Hand->Handedness == EHandedness::Right && Hand->ActiveGestureName == Binding.RightGesture && - Other->ActiveGestureName == Binding.LeftGesture); - - if (!(bLeftMatch || bRightMatch)) - continue; - - // both must be ready - if (Other->Phase != EGesturePhase::Started) - continue; - - // mark used - Hand->bUsedThisTick = true; - Other->bUsedThisTick = true; - - Hand->Phase = EGesturePhase::Ongoing; - Other->Phase = EGesturePhase::Ongoing; - - // spawn action (centralized factory recommended) - SpawnGestureAction(Binding.ActionName, Hand, Other); - - return; - } - } + if (Hand->bUsedThisTick) return; + if (TryExtendGestureAction(Hand)) return; + for (const FGestureBinding& Binding : GestureBindings) { - const bool bMatch = - (Hand->Handedness == EHandedness::Left && Binding.LeftGesture == Hand->ActiveGestureName) || - (Hand->Handedness == EHandedness::Right && Binding.RightGesture == Hand->ActiveGestureName); + bool bMatches = (Hand->Handedness == EHandedness::Left && Binding.LeftGesture == Hand->ActiveGestureName) + || (Hand->Handedness == EHandedness::Right && Binding.RightGesture == Hand->ActiveGestureName); - if (!bMatch) - continue; + if (!bMatches) continue; - // if paired hand is actively running something, allow extension check later - if (Hand->IsPaired() && Hand->PairedHand && Hand->PairedHand->Phase == EGesturePhase::Ongoing) - { - for (const FExtendedGestureBinding& Ext : Binding.ExentdedGestures) - { - if (Ext.GestureName != Hand->ActiveGestureName) - continue; + AGestureAction* NewAction = SpawnGestureAction(Binding.ActionName, Hand); + NewAction->InitAction(Hand, Binding, UEUnitsPerUnit); - if (Ext.GestureAction == Binding.ActionName) - { - // extend current action - ExtendGestureAction(Binding.ActionName, Hand); - Hand->bUsedThisTick = true; - return; - } - else - { - // branch: spawn new action linked to existing one - SpawnGestureActionWithParent(Ext.GestureAction, Hand, Hand->PairedHand); - Hand->bUsedThisTick = true; - return; - } - } - } - - // normal one-hand action - Hand->bUsedThisTick = true; - SpawnGestureAction(Binding.ActionName, Hand, nullptr); return; } - } -void UGestureManagerComponent::UpdateOngoingAction() +void UGestureManagerComponent::UpdateOngoingActions() { for (AGestureAction* Action : ActiveGestureActions) { @@ -338,18 +342,94 @@ void UGestureManagerComponent::UpdateOngoingAction() } } -void UGestureManagerComponent::SpawnGestureAction(FString ActionName, AHandActor* A, AHandActor* B) +AGestureAction* UGestureManagerComponent::SpawnGestureAction(FName ActionName, AHandActor* Hand) { + + TSubclassOf* ActionClassPtr = ActionClassMap.Find(ActionName); + + if (!ActionClassPtr || !Hand) return nullptr; + + FActorSpawnParameters SpawnParams; + SpawnParams.Owner = GetOwner(); + SpawnParams.SpawnCollisionHandlingOverride = + ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + + AGestureAction* NewAction = GetWorld()->SpawnActor( + *ActionClassPtr, + FVector::ZeroVector, + FRotator::ZeroRotator, + SpawnParams); + + if (!NewAction) return nullptr; + + + Hand->ActiveAction = NewAction; + Hand->bUsedThisTick = true; + Hand->Phase = EHandGesturePhase::Ongoing; + + return NewAction; } -void UGestureManagerComponent::ExtendGestureAction(FString ActionName, AHandActor* Hand) +bool UGestureManagerComponent::TryExtendGestureAction(AHandActor* Hand) { + if (!Hand->IsPaired() || Hand->PairedHand->Phase != EHandGesturePhase::Ongoing || !Hand->PairedHand->ActiveAction) return false; + + if (TryJoinExistingAction(Hand)) return true; + if (TryCreateExtension(Hand)) return true; + + + return false; } -void UGestureManagerComponent::SpawnGestureActionWithParent(FString ActionName, AHandActor* Hand, AHandActor* ParentHand) +bool UGestureManagerComponent::TryJoinExistingAction(AHandActor* Hand) { + AHandActor* OtherHand = Hand->PairedHand; + AGestureAction* OtherAction = OtherHand->ActiveAction; + + if (!OtherAction->bRequiresSecondHand) return false; // Check if its a Two Handed Action or an extension + + FGestureBinding& Binding = OtherAction->Binding; + + bool bMatches = (Hand->Handedness == EHandedness::Left && Binding.LeftGesture == Hand->ActiveGestureName) + || (Hand->Handedness == EHandedness::Right && Binding.RightGesture == Hand->ActiveGestureName); + + if (!bMatches) return false; + + OtherAction->AddOtherHand(Hand); + OtherHand->bUsedThisTick = true; + Hand->bUsedThisTick = true; + Hand->Phase = EHandGesturePhase::Ongoing; + Hand->ActiveAction = OtherAction; + + return true; } +bool UGestureManagerComponent::TryCreateExtension(AHandActor* Hand) +{ + AHandActor* OtherHand = Hand->PairedHand; + AGestureAction* OtherAction = OtherHand->ActiveAction; + if (!OtherHand->ActiveAction->bExtendable) return false; + FName ActionName; + for (const FExtendedGestureBinding& Gesture : OtherAction->Binding.ExtendedGestures) + { + if (Gesture.GestureName == Hand->ActiveGestureName) + { + ActionName = Gesture.GestureAction; + break; + } + } + if (ActionName.IsNone()) return false; + + AGestureAction* NewAction = SpawnGestureAction(ActionName, Hand); + + NewAction->InitAction(Hand, OtherHand->ActiveAction->Binding, UEUnitsPerUnit); + OtherHand->bUsedThisTick = true; + Hand->bUsedThisTick = true; + OtherAction->AddExtension(NewAction); + NewAction->AddExtension(OtherHand->ActiveAction); + + return true; +} diff --git a/Table/Source/Table/Private/HandActor.cpp b/Table/Source/Table/Private/HandActor.cpp index 82a48ce..ae0fb11 100644 --- a/Table/Source/Table/Private/HandActor.cpp +++ b/Table/Source/Table/Private/HandActor.cpp @@ -4,6 +4,7 @@ #include "HandManager.h" #include "FingerTip.h" #include "DrawDebugHelpers.h" +#include "GestureAction.h" static const int32 HandBones[][2] = { @@ -31,14 +32,21 @@ AHandActor::AHandActor() } +void AHandActor::ResetGesture() +{ + Phase = EHandGesturePhase::None; + ActiveGestureName = ""; + CandidateFrames = 0; + FailFrames = 0; + if (IsValid(ActiveAction)) ActiveAction->EndGesture(); + ActiveAction = nullptr; +} + // Called when the game starts or when spawned void AHandActor::BeginPlay() { Super::BeginPlay(); GetWorld()->GetTimerManager().SetTimer(DeathTimer, this, &AHandActor::HandDeath, DeathTime, true); - - - } // Called every frame @@ -123,8 +131,6 @@ void AHandActor::UpdateHand(const FHandPacketHand& NewHand) Confidence = NewHand.Confidence; HandId = NewHand.HandId; - //UE_LOG(LogTemp, Error, TEXT("Update Hand")); - for (int32 n = 0; n < 5; n++) { FVector f = Points[FingerTipBones[n]]; diff --git a/Table/Source/Table/Private/HandManager.cpp b/Table/Source/Table/Private/HandManager.cpp index 0fb206d..40d04ef 100644 --- a/Table/Source/Table/Private/HandManager.cpp +++ b/Table/Source/Table/Private/HandManager.cpp @@ -10,7 +10,7 @@ AHandManager::AHandManager() // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; GestureManager = CreateDefaultSubobject(TEXT("GestureManager")); - GestureManager->Initialize(GestureDefinitionTable, GestureActionTable); + } // Called when the game starts or when spawned @@ -18,6 +18,8 @@ void AHandManager::BeginPlay() { Super::BeginPlay(); + GestureManager->Initialize(GestureDefinitionTable, GestureActionTable); + if (!IsValid(HandActorClass)) { HandActorClass = AHandActor::StaticClass(); @@ -69,7 +71,7 @@ void AHandManager::AddDeadHand(AHandActor* DeadHand) DeadHands.Add(DeadHand); } -TArray AHandManager::GetHandList() +TArray& AHandManager::GetHandList() { return HandList; } diff --git a/Table/Source/Table/Private/MyGestureActions.cpp b/Table/Source/Table/Private/MyGestureActions.cpp new file mode 100644 index 0000000..345a3aa --- /dev/null +++ b/Table/Source/Table/Private/MyGestureActions.cpp @@ -0,0 +1,83 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "MyGestureActions.h" +#include "HandActor.h" +#include "TableIconActor.h" + + +void ARulerAction::UpdateAction_Implementation() +{ + if (!HandA) return; + + // Update end point to current fingertip + Anchors.Add("End", HandA->Points[8]); + + FVector Start = Anchors["Start"]; + FVector End = Anchors["End"]; + + float RawDist = FVector::Dist(Start, End); + float Inches = GetSnappedInches(RawDist); + + // Move line icon between start and end + if (Icons.IsValidIndex(0) && IsValid(Icons[0])) + { + Icons[0]->SetActorLocation((Start + End) * 0.5f); + // You'll add a SetEndpoints(Start, End) function to ATableIconLineActor later + } + + // Move label icon to midpoint, slightly above table + if (Icons.IsValidIndex(1) && IsValid(Icons[1])) + { + FVector Mid = (Start + End) * 0.5f + FVector(0, 0, 5.f); + Icons[1]->SetActorLocation(Mid); + // Icons[1]->SetLabelText(FString::Printf(TEXT("%d\""), (int32)Inches)); + } + + +} + +void ARulerAction::AbortAction_Implementation() +{ + Super::AbortAction(); +} + +void ARulerAction::AddExtension_Implementation(AGestureAction* OtherAction) +{ + return; +} + +void ARulerAction::EndGesture_Implementation() +{ + Super::EndGesture(); +} + +void ARulerAction::InitAction_Implementation(AHandActor* Hand, const FGestureBinding& NewBinding, float NewUEUnitsPerUnit) +{ + Super::InitAction(Hand, NewBinding, NewUEUnitsPerUnit); + Anchors.Add("Start", Hand->Points[8]); + + FActorSpawnParameters SpawnParams; + SpawnParams.Owner = GetOwner(); + SpawnParams.SpawnCollisionHandlingOverride = + ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + + AGestureAction* NewAction = nullptr; + /*AGestureAction* NewAction = GetWorld()->SpawnActor( + *ActionClassPtr, + FVector::ZeroVector, + FRotator::ZeroRotator, + SpawnParams); +*/ + if (!NewAction) return; + + +} + +float ARulerAction::GetSnappedInches(float RawDistance) const +{ + if (UEUnitsPerUnit <= 0.f) return 0.f; + float Unit = RawDistance / UEUnitsPerUnit; + return FMath::RoundToFloat(Unit); // snap to whole inches +} + diff --git a/Table/Source/Table/Private/MyTableIconActors.cpp b/Table/Source/Table/Private/MyTableIconActors.cpp new file mode 100644 index 0000000..93a4bd1 --- /dev/null +++ b/Table/Source/Table/Private/MyTableIconActors.cpp @@ -0,0 +1,167 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "MyTableIconActors.h" +#include "KismetProceduralMeshLibrary.h" +#include "TableIconActor.h" + +#include "Kismet/GameplayStatics.h" +#include "Camera/CameraActor.h" + +ATableIconLineActor::ATableIconLineActor() +{ + MeshComponent = CreateDefaultSubobject(TEXT("LineMesh")); + MeshComponent->SetupAttachment(RootComponent); + MeshComponent->SetCastShadow(false); +} + +void ATableIconLineActor::SetEndpoints(const FVector& Start, const FVector& End) +{ + // Skip rebuild if nothing meaningful changed + if (Start.Equals(LastStart, 0.1f) && End.Equals(LastEnd, 0.1f)) return; + LastStart = Start; + LastEnd = End; + + BuildLineMesh(Start, End); + if (bShowTickMarks) BuildTickMesh(Start, End); +} + +void ATableIconLineActor::BuildLineMesh(const FVector& Start, const FVector& End) +{ + FVector Dir = (End - Start).GetSafeNormal(); + FVector Up = FVector::UpVector; + FVector Right = FVector::CrossProduct(Dir, Up).GetSafeNormal() * LineThickness * 0.5f; + + // A flat quad along the line, facing up + TArray Vertices; + TArray Triangles; + TArray Normals; + TArray UVs; + TArray Colors; + TArray Tangents; + + Vertices.Add(Start - Right); // 0 + Vertices.Add(Start + Right); // 1 + Vertices.Add(End + Right); // 2 + Vertices.Add(End - Right); // 3 + + Triangles = { 0, 1, 2, 0, 2, 3 }; + + for (int i = 0; i < 4; i++) + { + Normals.Add(FVector::UpVector); + UVs.Add(FVector2D(i < 2 ? 0.f : 1.f, i % 2 == 0 ? 0.f : 1.f)); + Colors.Add(LineColor.ToFColor(true)); + Tangents.Add(FProcMeshTangent(Dir, false)); + } + + // Section 0 = main line + MeshComponent->CreateMeshSection(0, Vertices, Triangles, Normals, UVs, + Colors, Tangents, false); +} + +void ATableIconLineActor::BuildTickMesh(const FVector& Start, const FVector& End) +{ + if (UEUnitsPerUnit <= 0.f || TickInterval <= 0.f) return; + + float TotalDist = FVector::Dist(Start, End); + float TickSpacing = TickInterval * UEUnitsPerUnit; + int32 NumTicks = FMath::FloorToInt(TotalDist / TickSpacing); + + if (NumTicks <= 0) + { + MeshComponent->ClearMeshSection(1); + return; + } + + FVector Dir = (End - Start).GetSafeNormal(); + FVector Right = FVector::CrossProduct(Dir, FVector::UpVector).GetSafeNormal(); + + TArray Vertices; + TArray Triangles; + TArray Normals; + TArray UVs; + TArray Colors; + TArray Tangents; + + float TickHalfLen = LineThickness * 3.f; + float TickHalfW = LineThickness * 0.5f; + + for (int32 i = 1; i <= NumTicks; i++) + { + FVector Center = Start + Dir * (TickSpacing * i); + FVector A = Center - Right * TickHalfLen - Dir * TickHalfW; + FVector B = Center + Right * TickHalfLen - Dir * TickHalfW; + FVector C = Center + Right * TickHalfLen + Dir * TickHalfW; + FVector D = Center - Right * TickHalfLen + Dir * TickHalfW; + + int32 Base = Vertices.Num(); + Vertices.Add(A); Vertices.Add(B); + Vertices.Add(C); Vertices.Add(D); + + Triangles.Add(Base); Triangles.Add(Base + 1); Triangles.Add(Base + 2); + Triangles.Add(Base); Triangles.Add(Base + 2); Triangles.Add(Base + 3); + + for (int j = 0; j < 4; j++) + { + Normals.Add(FVector::UpVector); + UVs.Add(FVector2D::ZeroVector); + Colors.Add(LineColor.ToFColor(true)); + Tangents.Add(FProcMeshTangent(Dir, false)); + } + } + + // Section 1 = tick marks + MeshComponent->CreateMeshSection(1, Vertices, Triangles, Normals, UVs, + Colors, Tangents, false); +} + + + + + +ATableIconWidgetActor::ATableIconWidgetActor() +{ + PrimaryActorTick.bCanEverTick = true; + + WidgetComponent = CreateDefaultSubobject(TEXT("WidgetComponent")); + WidgetComponent->SetupAttachment(RootComponent); + WidgetComponent->SetDrawAtDesiredSize(true); + WidgetComponent->SetWidgetSpace(EWidgetSpace::World); + WidgetComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision); + + WidgetInteraction = CreateDefaultSubobject(TEXT("WidgetInteraction")); + WidgetInteraction->SetupAttachment(RootComponent); + WidgetInteraction->InteractionSource = EWidgetInteractionSource::Custom; + WidgetInteraction->InteractionDistance = 10.f; +} + +void ATableIconWidgetActor::Tick(float DeltaTime) +{ + Super::Tick(DeltaTime); + + if (!bFaceCamera) return; + + APlayerCameraManager* CamManager = UGameplayStatics::GetPlayerCameraManager(this, 0); + if (!CamManager) return; + + FVector CamLocation = CamManager->GetCameraLocation(); + FVector ToCamera = (CamLocation - GetActorLocation()).GetSafeNormal(); + + // Only rotate on yaw — keeps widget flat relative to table + ToCamera.Z = 0.f; + if (!ToCamera.IsNearlyZero()) + SetActorRotation(ToCamera.ToOrientationRotator()); +} + +void ATableIconWidgetActor::SetWidgetClass(TSubclassOf InWidgetClass) +{ + if (!InWidgetClass) return; + WidgetComponent->SetWidgetClass(InWidgetClass); + WidgetComponent->InitWidget(); +} + +UUserWidget* ATableIconWidgetActor::GetWidget() const +{ + return WidgetComponent->GetUserWidgetObject(); +} \ No newline at end of file diff --git a/Table/Source/Table/Private/TableIconActor.cpp b/Table/Source/Table/Private/TableIconActor.cpp new file mode 100644 index 0000000..94e9fcb --- /dev/null +++ b/Table/Source/Table/Private/TableIconActor.cpp @@ -0,0 +1,30 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "TableIconActor.h" + +// Sets default values +ATableIconActor::ATableIconActor() +{ + // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. + PrimaryActorTick.bCanEverTick = false; + +} + +// Called when the game starts or when spawned +void ATableIconActor::BeginPlay() +{ + Super::BeginPlay(); + +} + +void ATableIconActor::OnActionEnded_Implementation() +{ + if (!bPersistOnActionEnd) Destroy(); +} + +void ATableIconActor::OnActionUpdated_Implementation() +{ +} + + diff --git a/Table/Source/Table/Private/UserHUDWidget.cpp b/Table/Source/Table/Private/UserHUDWidget.cpp index d51f789..cdb053b 100644 --- a/Table/Source/Table/Private/UserHUDWidget.cpp +++ b/Table/Source/Table/Private/UserHUDWidget.cpp @@ -107,8 +107,8 @@ FContentSource UUserHUDWidget::ParseContentSource(TSharedPtr Source if (ContentTypeString == TEXT("Image")) Source.ContentType = EContentSourceType::Image; else if (ContentTypeString == TEXT("PDF")) Source.ContentType = EContentSourceType::PDF; - else if (ContentTypeString == TEXT("TextFile")) Source.ContentType = EContentSourceType::TextFile; - else if (ContentTypeString == TEXT("InlineText")) Source.ContentType = EContentSourceType::InlineText; + else if (ContentTypeString == TEXT("TextFile")) Source.ContentType = EContentSourceType::TextFile; + else if (ContentTypeString == TEXT("InlineText")) Source.ContentType = EContentSourceType::InlineText; else Source.ContentType = EContentSourceType::InlineText; return Source; diff --git a/Table/Source/Table/Public/GestureAction.h b/Table/Source/Table/Public/GestureAction.h index 8265c0c..44002c4 100644 --- a/Table/Source/Table/Public/GestureAction.h +++ b/Table/Source/Table/Public/GestureAction.h @@ -4,9 +4,12 @@ #include "CoreMinimal.h" #include "GameFramework/Actor.h" +#include "GestureData.h" #include "GestureAction.generated.h" class AHandActor; +class ATableIconActor; +class AHandManager; UCLASS() class TABLE_API AGestureAction : public AActor @@ -22,8 +25,21 @@ protected: virtual void BeginPlay() override; public: - // Called every frame - virtual void Tick(float DeltaTime) override; + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite) + float UEUnitsPerUnit = 100.f; + + UPROPERTY(EditDefaultsOnly) + bool bRequiresSecondHand = false; + + UPROPERTY(EditDefaultsOnly) + bool bExtendable = false; + + UPROPERTY(EditDefaultsOnly) + FGestureBinding Binding; + + UPROPERTY(BlueprintReadWrite) + TArray Icons; UPROPERTY(BlueprintReadWrite) TMap Anchors; @@ -34,15 +50,29 @@ public: UPROPERTY(BlueprintReadWrite) AHandActor* HandB; - UFUNCTION(BlueprintCallable) + UPROPERTY(BlueprintReadWrite) + AGestureAction* LinkedActions; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stuff") + AHandManager* HandManager; + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent) + void InitAction(AHandActor* Hand, const FGestureBinding& NewBinding, float NewUEUnitsPerUnit); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent) void UpdateAction(); - UFUNCTION(BlueprintCallable) + UFUNCTION(BlueprintCallable, BlueprintNativeEvent) void AbortAction(); - UFUNCTION(BlueprintCallable) - void AddExtension(AHandActor* OtherHand); + UFUNCTION(BlueprintCallable, BlueprintNativeEvent) + void AddExtension(AGestureAction* OtherAction); - UFUNCTION(BlueprintCallable) + UFUNCTION(BlueprintCallable, BlueprintNativeEvent) + void AddOtherHand(AHandActor* OtherHand); + + UFUNCTION(BlueprintCallable, BlueprintNativeEvent) void EndGesture(); + + void DestroyIcons(); }; diff --git a/Table/Source/Table/Public/GestureData.h b/Table/Source/Table/Public/GestureData.h index bff9fa1..890d4cf 100644 --- a/Table/Source/Table/Public/GestureData.h +++ b/Table/Source/Table/Public/GestureData.h @@ -19,7 +19,7 @@ enum class EHandedness : uint8 }; UENUM(BlueprintType) -enum class EGesturePhase : uint8 +enum class EHandGesturePhase : uint8 { None, Candidate, @@ -54,10 +54,10 @@ struct FExtendedGestureBinding GENERATED_BODY() UPROPERTY(BlueprintReadOnly) - FString GestureName; + FName GestureName; UPROPERTY(BlueprintReadOnly) - FString GestureAction; + FName GestureAction; }; // What gets parsed from the JSON @@ -67,15 +67,15 @@ struct FGestureBinding GENERATED_BODY() UPROPERTY(BlueprintReadOnly) - FString ActionName; + FName ActionName; UPROPERTY(BlueprintReadOnly) - FString LeftGesture; + FName LeftGesture; UPROPERTY(BlueprintReadOnly) - FString RightGesture; + FName RightGesture; UPROPERTY(BlueprintReadOnly) - TArray ExentdedGestures; + TArray ExtendedGestures; }; \ No newline at end of file diff --git a/Table/Source/Table/Public/GestureDefinition.h b/Table/Source/Table/Public/GestureDefinition.h index 08f1d69..49e562e 100644 --- a/Table/Source/Table/Public/GestureDefinition.h +++ b/Table/Source/Table/Public/GestureDefinition.h @@ -31,8 +31,7 @@ class TABLE_API UGestureDefinition : public UObject public: UPROPERTY(BlueprintReadWrite) - FString GestureName; - + FName GestureName; // This checks the gesture and returns an aproximate scor of how well the Gesture was exectuded, needs to be overwritten in child classes. UFUNCTION(BlueprintCallable) @@ -50,13 +49,96 @@ protected: float GetFingersAverage(const AHandActor* Hand, const TArray& Fingers, bool bExtended) const; FVector GetFingerDirection(const AHandActor* Hand, EHandFinger Finger) const; const FVector& GetPoint(const AHandActor* Hand, int32 Index) const; + bool IsFingerExtended(const AHandActor* Hand, EHandFinger Finger, float Threshold = 0.7f) const; }; UCLASS() -class TABLE_API UGesture_PinchPoint : public UGestureDefinition +class TABLE_API UGesture_PinchIndex : public UGestureDefinition { GENERATED_BODY() public: virtual float Evaluate(AHandActor* Hand) const override; + virtual void PostInitProperties() override + { + Super::PostInitProperties(); + + if (!HasAnyFlags(RF_ClassDefaultObject)) + { + GestureName = TEXT("PinchIndex"); + } + } +}; + +UCLASS() +class TABLE_API UGesture_PinchMiddleIndex : public UGestureDefinition +{ + GENERATED_BODY() + +public: + virtual float Evaluate(AHandActor* Hand) const override; + virtual void PostInitProperties() override + { + Super::PostInitProperties(); + + if (!HasAnyFlags(RF_ClassDefaultObject)) + { + GestureName = FName("PinchMiddle"); + } + } +}; + +UCLASS() +class TABLE_API UGesture_PointIndex : public UGestureDefinition +{ + GENERATED_BODY() + +public: + virtual float Evaluate(AHandActor* Hand) const override; + virtual void PostInitProperties() override + { + Super::PostInitProperties(); + + if (!HasAnyFlags(RF_ClassDefaultObject)) + { + GestureName = FName("PointIndex"); + } + } +}; + + +UCLASS() +class TABLE_API UGesture_PointMiddle : public UGestureDefinition +{ + GENERATED_BODY() + +public: + virtual float Evaluate(AHandActor* Hand) const override; + virtual void PostInitProperties() override + { + Super::PostInitProperties(); + + if (!HasAnyFlags(RF_ClassDefaultObject)) + { + GestureName = TEXT("PointMiddle"); + } + } +}; + +UCLASS() +class TABLE_API UGesture_PointIndexMiddle : public UGestureDefinition +{ + GENERATED_BODY() + +public: + virtual float Evaluate(AHandActor* Hand) const override; + virtual void PostInitProperties() override + { + Super::PostInitProperties(); + + if (!HasAnyFlags(RF_ClassDefaultObject)) + { + GestureName = TEXT("PointIndexMiddle"); + } + } }; \ No newline at end of file diff --git a/Table/Source/Table/Public/GestureManagerComponent.h b/Table/Source/Table/Public/GestureManagerComponent.h index 22bffc4..d736a27 100644 --- a/Table/Source/Table/Public/GestureManagerComponent.h +++ b/Table/Source/Table/Public/GestureManagerComponent.h @@ -7,7 +7,7 @@ #include "GestureData.h" #include "GestureManagerComponent.generated.h" - +class AHandManager; UCLASS(ClassGroup = "Gesture", BlueprintType, Blueprintable, ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) class TABLE_API UGestureManagerComponent : public UActorComponent @@ -24,8 +24,11 @@ protected: public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Scale") + float UEUnitsPerUnit = 100.f; // set via calibration rectangle + UPROPERTY(BlueprintReadOnly) - class AHandManager* HandManager; + AHandManager* HandManager; UFUNCTION(BlueprintCallable, Category = "Gesture") void EvaluateHands(); @@ -57,13 +60,13 @@ public: // Populated on BeginPlay from the tables UPROPERTY(BlueprintReadOnly, Category = "Gesture") - TMap DefinitionMap; + TMap DefinitionMap; UPROPERTY(BlueprintReadOnly, Category = "Gesture") - TMap> ActionClassMap; + TMap> ActionClassMap; private: - // GestureManager.h — add these + // UPROPERTY(EditDefaultsOnly, Category = "Gesture") UDataTable* GestureDefinitionTable; @@ -73,12 +76,13 @@ private: void CheckGestureDefinitions(TArray& Hands); void TriggerAction(AHandActor* Hand); - void UpdateOngoingAction(); - void EndGesture(); + void UpdateOngoingActions(); - void SpawnGestureAction(FString ActionName, AHandActor* A, AHandActor* B); - void ExtendGestureAction(FString ActionName, AHandActor* Hand); - void SpawnGestureActionWithParent(FString ActionName, AHandActor* Hand, AHandActor* ParentHand); + AGestureAction* SpawnGestureAction(FName ActionName, AHandActor* Hand); + bool TryExtendGestureAction(AHandActor* Hand); + + bool TryJoinExistingAction(AHandActor* Hand); + bool TryCreateExtension(AHandActor* Hand); static constexpr int32 CandidateThreshold = 8; static constexpr int32 FailThreshold = 5; diff --git a/Table/Source/Table/Public/HandActor.h b/Table/Source/Table/Public/HandActor.h index 61fc8aa..e9573df 100644 --- a/Table/Source/Table/Public/HandActor.h +++ b/Table/Source/Table/Public/HandActor.h @@ -11,6 +11,7 @@ class AHandManager; class AFingerTip; +class AGestureAction; UCLASS() class TABLE_API AHandActor : public AActor @@ -47,10 +48,13 @@ public: // Gestures UPROPERTY(BlueprintReadWrite) - EGesturePhase Phase = EGesturePhase::None; + EHandGesturePhase Phase = EHandGesturePhase::None; UPROPERTY(BlueprintReadWrite) - FString ActiveGestureName; + FName ActiveGestureName; + + UPROPERTY(BlueprintReadWrite) + AGestureAction* ActiveAction; UPROPERTY(BlueprintReadWrite) TMap Anchors; @@ -67,15 +71,7 @@ public: bool bUsedThisTick = false; bool IsPaired() const { return PairedHand != nullptr; } - - void ResetGesture() - { - Phase = EGesturePhase::None; - ActiveGestureName = ""; - CandidateFrames = 0; - FailFrames = 0; - } - + void ResetGesture(); protected: // Called when the game starts or when spawned diff --git a/Table/Source/Table/Public/HandManager.h b/Table/Source/Table/Public/HandManager.h index c77e83d..39966ac 100644 --- a/Table/Source/Table/Public/HandManager.h +++ b/Table/Source/Table/Public/HandManager.h @@ -62,7 +62,7 @@ public: FOnHandRemoved OnHandRemoved; UFUNCTION(BlueprintCallable) - TArray GetHandList(); + TArray& GetHandList(); //Gestures @@ -72,12 +72,13 @@ public: const TArray& GetHandList() const { return HandList; } // GestureManager.h — add these - UPROPERTY(EditDefaultsOnly, Category = "Gesture") + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gesture") UDataTable* GestureDefinitionTable; - UPROPERTY(EditDefaultsOnly, Category = "Gesture") + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gesture") UDataTable* GestureActionTable; + private: void RemoveDeadHands(); void UpdateHands(); diff --git a/Table/Source/Table/Public/MyGestureActions.h b/Table/Source/Table/Public/MyGestureActions.h new file mode 100644 index 0000000..31282d6 --- /dev/null +++ b/Table/Source/Table/Public/MyGestureActions.h @@ -0,0 +1,36 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "GestureAction.h" +#include "MyGestureActions.generated.h" + +/** + * + */ +UCLASS() +class TABLE_API AMyGestureActions : public AGestureAction +{ + GENERATED_BODY() + +}; + + +UCLASS() +class TABLE_API ARulerAction : public AGestureAction +{ + GENERATED_BODY() + +public: + void UpdateAction_Implementation() override; + void AbortAction_Implementation() override; + void AddExtension_Implementation(AGestureAction* OtherAction) override; + void EndGesture_Implementation() override; + void InitAction_Implementation(AHandActor* Hand, const FGestureBinding& NewBinding, float NewUEUnitsPerUnit) override; + +private: + float GetSnappedInches(float RawDistance) const; +}; + + diff --git a/Table/Source/Table/Public/MyTableIconActors.h b/Table/Source/Table/Public/MyTableIconActors.h new file mode 100644 index 0000000..a0a6c99 --- /dev/null +++ b/Table/Source/Table/Public/MyTableIconActors.h @@ -0,0 +1,93 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "TableIconActor.h" +#include "ProceduralMeshComponent.h" +#include "Components/WidgetComponent.h" +#include "Components/WidgetInteractionComponent.h" +#include "MyTableIconActors.generated.h" + +class UWidgetComponent; +class UWidgetInteractionComponent; +/** + * + */ +UCLASS() +class TABLE_API AMyTableIconActors : public ATableIconActor +{ + GENERATED_BODY() + +}; + + +UCLASS(Blueprintable) +class TABLE_API ATableIconLineActor : public ATableIconActor +{ + GENERATED_BODY() + +public: + ATableIconLineActor(); + + UFUNCTION(BlueprintCallable) + void SetEndpoints(const FVector& Start, const FVector& End); + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite) + float LineThickness = 1.f; + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite) + FLinearColor LineColor = FLinearColor::White; + + // Tick markers along the line — e.g. every inch + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite) + bool bShowTickMarks = true; + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite) + float TickInterval = 1.f; // in real-world units + + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite) + float UEUnitsPerUnit = 100.f; + +private: + UPROPERTY() + UProceduralMeshComponent* MeshComponent; + + void BuildLineMesh(const FVector& Start, const FVector& End); + void BuildTickMesh(const FVector& Start, const FVector& End); + + FVector LastStart; + FVector LastEnd; +}; + + + +UCLASS(Blueprintable) +class TABLE_API ATableIconWidgetActor : public ATableIconActor +{ + GENERATED_BODY() + +public: + ATableIconWidgetActor(); + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly) + UWidgetComponent* WidgetComponent; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly) + UWidgetInteractionComponent* WidgetInteraction; + + // Face toward a target each tick — useful for always facing the camera + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite) + bool bFaceCamera = true; + + // Set widget content — callable from GestureAction + UFUNCTION(BlueprintCallable) + void SetWidgetClass(TSubclassOf InWidgetClass); + + // Get the underlying widget cast to a specific type in BP + UFUNCTION(BlueprintCallable, BlueprintPure) + UUserWidget* GetWidget() const; + +protected: + virtual void Tick(float DeltaTime) override; +}; \ No newline at end of file diff --git a/Table/Source/Table/Public/TableIconActor.h b/Table/Source/Table/Public/TableIconActor.h new file mode 100644 index 0000000..d347c02 --- /dev/null +++ b/Table/Source/Table/Public/TableIconActor.h @@ -0,0 +1,36 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "TableIconActor.generated.h" + +UCLASS() +class TABLE_API ATableIconActor : public AActor +{ + GENERATED_BODY() + +public: + // Sets default values for this actor's properties + ATableIconActor(); + +protected: + // Called when the game starts or when spawned + virtual void BeginPlay() override; + +public: + // If true, GestureAction will not destroy this on EndGesture + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite) + bool bPersistOnActionEnd = false; + + // Called by GestureAction on end — BP overrides this for fade/lock behavior + UFUNCTION(BlueprintNativeEvent) + void OnActionEnded(); + + // Called by GestureAction every tick + UFUNCTION(BlueprintNativeEvent) + void OnActionUpdated(); + + +}; diff --git a/Table/Source/Table/Table.Build.cs b/Table/Source/Table/Table.Build.cs index 6d91b4c..f23f065 100644 --- a/Table/Source/Table/Table.Build.cs +++ b/Table/Source/Table/Table.Build.cs @@ -20,7 +20,8 @@ public class Table : ModuleRules "UMG" , "Json", "JsonUtilities", - "ImageWrapper" + "ImageWrapper", + "ProceduralMeshComponent" }); PrivateDependencyModuleNames.AddRange(new string[] { }); diff --git a/Table/Source/TableEditor.Target.cs b/Table/Source/TableEditor.Target.cs index 14b5747..52dd73f 100644 --- a/Table/Source/TableEditor.Target.cs +++ b/Table/Source/TableEditor.Target.cs @@ -8,8 +8,8 @@ public class TableEditorTarget : TargetRules public TableEditorTarget( TargetInfo Target) : base(Target) { Type = TargetType.Editor; - DefaultBuildSettings = BuildSettingsVersion.V6; - IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_7; + DefaultBuildSettings = BuildSettingsVersion.V7; + IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_8; ExtraModuleNames.Add("Table"); } } diff --git a/Table/Table.slnx b/Table/Table.slnx new file mode 100644 index 0000000..0da5274 --- /dev/null +++ b/Table/Table.slnx @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Table/Table.uproject b/Table/Table.uproject index d0f330c..b9081c3 100644 --- a/Table/Table.uproject +++ b/Table/Table.uproject @@ -1,6 +1,6 @@ { "FileVersion": 3, - "EngineAssociation": "5.7", + "EngineAssociation": "5.8", "Category": "", "Description": "", "Modules": [ @@ -22,7 +22,7 @@ }, { "Name": "VisualStudioTools", - "Enabled": true, + "Enabled": false, "SupportedTargetPlatforms": [ "Win64" ],