first commit
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "ContentNodeWidget.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "IImageWrapper.h"
|
||||
#include "IImageWrapperModule.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
|
||||
FString UContentNodeWidget::ResolvePath(const FString& RelativePath)
|
||||
{
|
||||
//return FPaths::ConvertRelativePathToFull(BaseFolderPath + RelativePath);
|
||||
FString FullPath = FPaths::ConvertRelativePathToFull(
|
||||
FPaths::Combine(BaseFolderPath, RelativePath)
|
||||
);
|
||||
|
||||
return FullPath;
|
||||
}
|
||||
|
||||
UTexture2D* UContentNodeWidget::LoadTexture(const FString& RelativePath)
|
||||
{
|
||||
FString FullPath = ResolvePath(RelativePath);
|
||||
return LoadTextureFromPath(FullPath); // see below
|
||||
}
|
||||
|
||||
FString UContentNodeWidget::LoadTextFile(const FString& RelativePath)
|
||||
{
|
||||
FString FullPath = ResolvePath(RelativePath);
|
||||
FString Result;
|
||||
|
||||
if (!FFileHelper::LoadFileToString(Result, *FullPath))
|
||||
UE_LOG(LogTemp, Warning, TEXT("ContentNodeWidget: failed to load text: %s"), *FullPath);
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
TArray<UTexture2D*> UContentNodeWidget::LoadPDFPages(const FString& RelativePath)
|
||||
{
|
||||
TArray<UTexture2D*> Pages;
|
||||
|
||||
// Expects PNGs named: rulebook_p01.png, rulebook_p02.png etc.
|
||||
// alongside the .pdf — generated by your Python converter offline
|
||||
FString BaseName = FPaths::GetBaseFilename(RelativePath);
|
||||
FString Folder = FPaths::GetPath(ResolvePath(RelativePath));
|
||||
|
||||
TArray<FString> PageFiles;
|
||||
IFileManager::Get().FindFiles(PageFiles, *Folder, TEXT("*.png"));
|
||||
PageFiles.Sort(); // ensures p01, p02, p03 order
|
||||
|
||||
for (const FString& PageFile : PageFiles)
|
||||
{
|
||||
// Only grab files that belong to this PDF by checking prefix
|
||||
if (!PageFile.StartsWith(BaseName)) continue;
|
||||
|
||||
UTexture2D* Tex = LoadTextureFromPath(Folder / PageFile);
|
||||
|
||||
|
||||
|
||||
if (Tex) {
|
||||
Tex->CompressionSettings = TC_EditorIcon; // or TC_UserInterface2D if available
|
||||
Tex->MipGenSettings = TMGS_NoMipmaps;
|
||||
Tex->Filter = TF_Nearest; // or TF_Default if too sharp
|
||||
Tex->SRGB = true;
|
||||
|
||||
Tex->UpdateResource();
|
||||
|
||||
Pages.Add(Tex);
|
||||
}
|
||||
}
|
||||
|
||||
return Pages;
|
||||
}
|
||||
|
||||
// Internal helper — loads any PNG/JPG from an absolute path at runtime
|
||||
UTexture2D* UContentNodeWidget::LoadTextureFromPath(const FString& FullPath)
|
||||
{
|
||||
TArray<uint8> FileData;
|
||||
if (!FFileHelper::LoadFileToArray(FileData, *FullPath))
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("ContentNodeWidget: failed to read file: %s"), *FullPath);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
IImageWrapperModule& ImageWrapperModule =
|
||||
FModuleManager::LoadModuleChecked<IImageWrapperModule>(TEXT("ImageWrapper"));
|
||||
|
||||
EImageFormat Format = ImageWrapperModule.DetectImageFormat(FileData.GetData(), FileData.Num());
|
||||
TSharedPtr<IImageWrapper> ImageWrapper = ImageWrapperModule.CreateImageWrapper(Format);
|
||||
|
||||
if (!ImageWrapper.IsValid() || !ImageWrapper->SetCompressed(FileData.GetData(), FileData.Num()))
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("ContentNodeWidget: failed to decode image: %s"), *FullPath);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TArray<uint8> RawData;
|
||||
ImageWrapper->GetRaw(ERGBFormat::BGRA, 8, RawData);
|
||||
|
||||
UTexture2D* Texture = UTexture2D::CreateTransient(
|
||||
ImageWrapper->GetWidth(),
|
||||
ImageWrapper->GetHeight(),
|
||||
PF_B8G8R8A8
|
||||
);
|
||||
|
||||
void* TextureData = Texture->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_WRITE);
|
||||
FMemory::Memcpy(TextureData, RawData.GetData(), RawData.Num());
|
||||
Texture->GetPlatformData()->Mips[0].BulkData.Unlock();
|
||||
Texture->UpdateResource();
|
||||
|
||||
return Texture;
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
|
||||
#include "DetectionManager.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
#include "Sockets.h"
|
||||
#include "SocketSubsystem.h"
|
||||
#include "Common/UdpSocketBuilder.h"
|
||||
#include "Common/UdpSocketReceiver.h"
|
||||
#include "Networking.h"
|
||||
|
||||
|
||||
|
||||
// MediaPipe Hands connections (20 bones)
|
||||
const int32 ADetectionManager::HandConnections[20][2] = {
|
||||
{0,1},{1,2},{2,3},{3,4},
|
||||
{0,5},{5,6},{6,7},{7,8},
|
||||
{0,9},{9,10},{10,11},{11,12},
|
||||
{0,13},{13,14},{14,15},{15,16},
|
||||
{0,17},{17,18},{18,19},{19,20}
|
||||
};
|
||||
|
||||
ADetectionManager::ADetectionManager()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
}
|
||||
|
||||
void ADetectionManager::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
if (bStartReceiverOnBeginPlay)
|
||||
{
|
||||
StartUdpReceiver();
|
||||
}
|
||||
}
|
||||
|
||||
void ADetectionManager::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
StopUdpReceiver();
|
||||
Super::EndPlay(EndPlayReason);
|
||||
}
|
||||
|
||||
void ADetectionManager::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
|
||||
}
|
||||
|
||||
void ADetectionManager::StartUdpReceiver()
|
||||
{
|
||||
if (Socket)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FIPv4Endpoint Endpoint(FIPv4Address::Any, ListenPort);
|
||||
|
||||
Socket = FUdpSocketBuilder(TEXT("HandTrackingUdpSocket"))
|
||||
.AsNonBlocking()
|
||||
.AsReusable()
|
||||
.BoundToEndpoint(Endpoint)
|
||||
.WithReceiveBufferSize(2 * 1024 * 1024);
|
||||
|
||||
if (!Socket)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[DetectionManager] Failed to create UDP socket on port %d"), ListenPort);
|
||||
return;
|
||||
}
|
||||
|
||||
const FTimespan ThreadWaitTime = FTimespan::FromMilliseconds(2);
|
||||
Receiver = MakeShared<FUdpSocketReceiver>(Socket, ThreadWaitTime, TEXT("HandTrackingUdpReceiver"));
|
||||
Receiver->OnDataReceived().BindUObject(this, &ADetectionManager::OnUdpPacketReceived);
|
||||
Receiver->Start();
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("[DetectionManager] UDP receiver started on port %d"), ListenPort);
|
||||
}
|
||||
|
||||
void ADetectionManager::StopUdpReceiver()
|
||||
{
|
||||
if (Receiver.IsValid())
|
||||
{
|
||||
Receiver->Stop();
|
||||
Receiver.Reset();
|
||||
}
|
||||
|
||||
if (Socket)
|
||||
{
|
||||
Socket->Close();
|
||||
ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(Socket);
|
||||
Socket = nullptr;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("[DetectionManager] UDP receiver stopped"));
|
||||
}
|
||||
|
||||
void ADetectionManager::OnUdpPacketReceived(
|
||||
const TSharedPtr<FArrayReader, ESPMode::ThreadSafe>& ArrayReader,
|
||||
const FIPv4Endpoint& EndPt
|
||||
)
|
||||
{
|
||||
|
||||
if (!ArrayReader.IsValid() || ArrayReader->Num() <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<uint8> Bytes;
|
||||
Bytes.SetNumUninitialized(ArrayReader->Num());
|
||||
FMemory::Memcpy(Bytes.GetData(), ArrayReader->GetData(), ArrayReader->Num());
|
||||
|
||||
int32 Seq = 0;
|
||||
uint64 TsMs = 0;
|
||||
TArray<FHandPacketHand> Hands;
|
||||
|
||||
if (!ParseHandPacket(Bytes, Seq, TsMs, Hands))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
FScopeLock Lock(&DataMutex);
|
||||
LatestSeq = Seq;
|
||||
LatestTimestampMs = TsMs;
|
||||
LatestHands = MoveTemp(Hands);
|
||||
bHasNewData = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---- Little-endian readers ----
|
||||
static uint32 ReadU32LE(const uint8* Ptr)
|
||||
{
|
||||
return (uint32)Ptr[0] | ((uint32)Ptr[1] << 8) | ((uint32)Ptr[2] << 16) | ((uint32)Ptr[3] << 24);
|
||||
}
|
||||
|
||||
static uint64 ReadU64LE(const uint8* Ptr)
|
||||
{
|
||||
uint64 v = 0;
|
||||
for (int i = 0; i < 8; ++i) v |= ((uint64)Ptr[i] << (8 * i));
|
||||
return v;
|
||||
}
|
||||
|
||||
static float ReadF32LE(const uint8* Ptr)
|
||||
{
|
||||
uint32 u = ReadU32LE(Ptr);
|
||||
float f;
|
||||
FMemory::Memcpy(&f, &u, sizeof(float));
|
||||
return f;
|
||||
}
|
||||
|
||||
bool ADetectionManager::ParseHandPacket(const TArray<uint8>& Bytes, int32& OutSeq, uint64& OutTsMs, TArray<FHandPacketHand>& OutHands)
|
||||
{
|
||||
// Header: <4s B I Q B
|
||||
// per hand: <B B B f + point_count*3 float32
|
||||
|
||||
const int32 MinHeaderSize = 4 + 1 + 4 + 8 + 1;
|
||||
if (Bytes.Num() < MinHeaderSize)
|
||||
return false;
|
||||
|
||||
const uint8* Ptr = Bytes.GetData();
|
||||
const uint8* End = Ptr + Bytes.Num();
|
||||
|
||||
// magic
|
||||
if (!(Ptr[0] == 'H' && Ptr[1] == 'A' && Ptr[2] == 'N' && Ptr[3] == 'D'))
|
||||
return false;
|
||||
Ptr += 4;
|
||||
|
||||
const uint8 Version = *Ptr++;
|
||||
// Accept either version 1 (old) or 2 (new), or enforce one
|
||||
//if (Version != 2) // set to 2 if you bump Python VERSION
|
||||
// return false;
|
||||
|
||||
OutSeq = (int32)ReadU32LE(Ptr); Ptr += 4;
|
||||
OutTsMs = ReadU64LE(Ptr); Ptr += 8;
|
||||
|
||||
const uint8 HandCount = *Ptr++;
|
||||
OutHands.Reset();
|
||||
OutHands.Reserve(HandCount);
|
||||
|
||||
for (uint8 hi = 0; hi < HandCount; ++hi)
|
||||
{
|
||||
// Need at least: hand_id + point_count + handedness + confidence
|
||||
if (Ptr + 1 + 1 + 1 + 4 > End) return false;
|
||||
|
||||
const uint8 HandId = *Ptr++;
|
||||
const uint8 PointCount = *Ptr++;
|
||||
|
||||
const uint8 HandednessByte = *Ptr++;
|
||||
const float Confidence = ReadF32LE(Ptr); Ptr += 4;
|
||||
|
||||
const int64 NeedBytes = (int64)PointCount * 3 * 4;
|
||||
if (Ptr + NeedBytes > End) return false;
|
||||
|
||||
if (PointCount != 21)
|
||||
{
|
||||
Ptr += NeedBytes;
|
||||
continue;
|
||||
}
|
||||
|
||||
FHandPacketHand Hand;
|
||||
Hand.HandId = (int32)HandId;
|
||||
Hand.Handedness = (HandednessByte == 1) ? EHandedness::Left :
|
||||
(HandednessByte == 2) ? EHandedness::Right :
|
||||
EHandedness::Unknown;
|
||||
Hand.Confidence = Confidence;
|
||||
|
||||
Hand.Points.SetNum(PointCount);
|
||||
|
||||
for (int32 pi = 0; pi < PointCount; ++pi)
|
||||
{
|
||||
const float X = ReadF32LE(Ptr); Ptr += 4;
|
||||
const float Y = ReadF32LE(Ptr); Ptr += 4;
|
||||
const float Z = ReadF32LE(Ptr); Ptr += 4;
|
||||
|
||||
Hand.Points[pi] = FVector(X, Y, -Z);
|
||||
}
|
||||
|
||||
OutHands.Add(MoveTemp(Hand));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
float ADetectionManager::MapDepthToRadiusCm(float Z) const
|
||||
{
|
||||
if (!bRadiusFromDepth)
|
||||
return BasePointRadiusCm;
|
||||
|
||||
const float Zc = FMath::Clamp(Z, DepthNear, DepthFar);
|
||||
const float T = (Zc - DepthNear) / FMath::Max(1.0f, (DepthFar - DepthNear)); // 0..1
|
||||
return FMath::Lerp(RadiusNearCm, RadiusFarCm, T);
|
||||
}
|
||||
|
||||
bool ADetectionManager::ConsumeLatestHands(TArray<FHandPacketHand>& OutHands, int32& OutSeq, int64& OutTimestampMs)
|
||||
{
|
||||
FScopeLock Lock(&DataMutex);
|
||||
if (!bHasNewData)
|
||||
{
|
||||
OutHands.Reset();
|
||||
OutSeq = 0;
|
||||
OutTimestampMs = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Atomically "consume" the latest frame
|
||||
bHasNewData = false;
|
||||
OutSeq = LatestSeq;
|
||||
OutTimestampMs = (int64)LatestTimestampMs;
|
||||
|
||||
OutHands = MoveTemp(LatestHands); // takes ownership, leaves LatestHands empty
|
||||
return OutHands.Num() > 0;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "FingerTip.h"
|
||||
|
||||
// Sets default values
|
||||
AFingerTip::AFingerTip()
|
||||
{
|
||||
// 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;
|
||||
|
||||
}
|
||||
|
||||
// Called when the game starts or when spawned
|
||||
void AFingerTip::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
}
|
||||
|
||||
// Called every frame
|
||||
void AFingerTip::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "GestureAction.h"
|
||||
#include "HandActor.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;
|
||||
|
||||
}
|
||||
|
||||
// Called when the game starts or when spawned
|
||||
void AGestureAction::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
}
|
||||
|
||||
// Called every frame
|
||||
void AGestureAction::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
|
||||
}
|
||||
|
||||
void AGestureAction::UpdateAction()
|
||||
{
|
||||
Destroy();
|
||||
}
|
||||
|
||||
void AGestureAction::AbortAction()
|
||||
{
|
||||
}
|
||||
|
||||
void AGestureAction::AddExtension(AHandActor* OtherHand)
|
||||
{
|
||||
HandB = OtherHand;
|
||||
}
|
||||
|
||||
void AGestureAction::EndGesture()
|
||||
{
|
||||
Destroy();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "GestureData.h"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "GestureDefinition.h"
|
||||
#include "HandActor.h"
|
||||
|
||||
float UGestureDefinition::Evaluate(AHandActor* Hand) const
|
||||
{
|
||||
return 0.f;
|
||||
}
|
||||
|
||||
float UGesture_PinchPoint::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
|
||||
);
|
||||
|
||||
return
|
||||
Pinch * 0.5f +
|
||||
IndexExtended * 0.3f +
|
||||
OthersCurled * 0.2f;
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "GestureManagerComponent.h"
|
||||
#include "HandManager.h"
|
||||
#include "GestureDefinition.h"
|
||||
#include "GestureAction.h"
|
||||
#include "HandActor.h"
|
||||
|
||||
// Sets default values for this component's properties
|
||||
UGestureManagerComponent::UGestureManagerComponent()
|
||||
{
|
||||
// Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features
|
||||
// off to improve performance if you don't need them.
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
|
||||
HandManager = Cast<AHandManager>(GetOwner());
|
||||
}
|
||||
|
||||
|
||||
// Called when the game starts
|
||||
void UGestureManagerComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
HandManager->OnHandAdded.AddDynamic(this, &UGestureManagerComponent::OnHandAdded);
|
||||
HandManager->OnHandRemoved.AddDynamic(this, &UGestureManagerComponent::OnHandRemoved);
|
||||
}
|
||||
|
||||
void UGestureManagerComponent::EvaluateHands()
|
||||
{
|
||||
TArray<AHandActor*>& Hands = HandManager->HandList;
|
||||
|
||||
for (AHandActor* Hand : Hands) Hand->bUsedThisTick = false;
|
||||
|
||||
CheckGestureDefinitions(Hands);
|
||||
|
||||
for (AHandActor* Hand : Hands)
|
||||
{
|
||||
if (Hand->bUsedThisTick) continue;
|
||||
switch (Hand->Phase)
|
||||
{
|
||||
case EGesturePhase::Started:
|
||||
TriggerAction(Hand);
|
||||
break;
|
||||
case EGesturePhase::Ended:
|
||||
Hand->ResetGesture();
|
||||
EndGesture();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateOngoingAction();
|
||||
}
|
||||
|
||||
void UGestureManagerComponent::OnHandAdded(AHandActor* Hand)
|
||||
{
|
||||
Hand->ResetGesture();
|
||||
}
|
||||
|
||||
void UGestureManagerComponent::OnHandRemoved(AHandActor* Hand)
|
||||
{
|
||||
Hand->PairedHand->PairedHand = nullptr;
|
||||
Hand->ResetGesture();
|
||||
}
|
||||
|
||||
void UGestureManagerComponent::CheckGestureDefinitions(TArray<AHandActor*>& Hands)
|
||||
{
|
||||
const float MinScoreThreshold = 0.5f;
|
||||
|
||||
for (AHandActor* Hand : Hands) {
|
||||
float BestScore = 0.f;
|
||||
FString BestGesture = "";
|
||||
|
||||
for (UGestureDefinition* Gesture : GestureDefinitions) {
|
||||
|
||||
float Score = Gesture->Evaluate(Hand);
|
||||
|
||||
if (Score > BestScore)
|
||||
{
|
||||
BestScore = Score;
|
||||
BestGesture = Gesture->GestureName;
|
||||
}
|
||||
}
|
||||
|
||||
if (BestScore < MinScoreThreshold)
|
||||
{
|
||||
if (Hand->Phase != EGesturePhase::None)
|
||||
{
|
||||
Hand->FailFrames++;
|
||||
|
||||
if (Hand->FailFrames >= FailThreshold)
|
||||
{
|
||||
Hand->Phase = EGesturePhase::Ended;
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (Hand->Phase)
|
||||
{
|
||||
case EGesturePhase::None:
|
||||
Hand->ActiveGestureName = BestGesture;
|
||||
Hand->Phase = EGesturePhase::Candidate;
|
||||
Hand->CandidateFrames = 1;
|
||||
Hand->FailFrames = 0;
|
||||
|
||||
break;
|
||||
case EGesturePhase::Candidate:
|
||||
if (Hand->ActiveGestureName != BestGesture)
|
||||
{
|
||||
Hand->FailFrames++;
|
||||
break;
|
||||
}
|
||||
|
||||
Hand->CandidateFrames++;
|
||||
Hand->FailFrames = 0;
|
||||
|
||||
if (Hand->CandidateFrames >= CandidateThreshold)
|
||||
Hand->Phase = EGesturePhase::Started;
|
||||
|
||||
break;
|
||||
case EGesturePhase::Ongoing:
|
||||
if (Hand->ActiveGestureName != BestGesture)
|
||||
{
|
||||
Hand->FailFrames++;
|
||||
|
||||
if (Hand->FailFrames >= FailThreshold)
|
||||
{
|
||||
Hand->Phase = EGesturePhase::Ended;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Hand->FailFrames = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (Hand->FailFrames >= FailThreshold)
|
||||
Hand->Phase = EGesturePhase::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;
|
||||
}
|
||||
}
|
||||
|
||||
for (const FGestureBinding& Binding : GestureBindings)
|
||||
{
|
||||
const bool bMatch =
|
||||
(Hand->Handedness == EHandedness::Left && Binding.LeftGesture == Hand->ActiveGestureName) ||
|
||||
(Hand->Handedness == EHandedness::Right && Binding.RightGesture == Hand->ActiveGestureName);
|
||||
|
||||
if (!bMatch)
|
||||
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;
|
||||
|
||||
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()
|
||||
{
|
||||
for (AGestureAction* Action : ActiveGestureActions)
|
||||
{
|
||||
if (!Action) continue;
|
||||
|
||||
Action->UpdateAction();
|
||||
}
|
||||
}
|
||||
|
||||
void UGestureManagerComponent::SpawnGestureAction(FString ActionName, AHandActor* A, AHandActor* B)
|
||||
{
|
||||
}
|
||||
|
||||
void UGestureManagerComponent::ExtendGestureAction(FString ActionName, AHandActor* Hand)
|
||||
{
|
||||
}
|
||||
|
||||
void UGestureManagerComponent::SpawnGestureActionWithParent(FString ActionName, AHandActor* Hand, AHandActor* ParentHand)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#include "HandActor.h"
|
||||
#include "HandManager.h"
|
||||
#include "FingerTip.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
|
||||
|
||||
static const int32 HandBones[][2] = {
|
||||
{0,1},{1,2},{2,3},{3,4},
|
||||
{0,5},{5,6},{6,7},{7,8},
|
||||
{0,9},{9,10},{10,11},{11,12},
|
||||
{0,13},{13,14},{14,15},{15,16},
|
||||
{0,17},{17,18},{18,19},{19,20},
|
||||
{5,9},{9,13},{13,17}
|
||||
};
|
||||
|
||||
static const int32 FingerTipBones[] = {
|
||||
4, 8, 12, 16, 20
|
||||
};
|
||||
|
||||
static const int32 HandBoneCount = sizeof(HandBones) / sizeof(HandBones[0]);
|
||||
|
||||
static const float Life = 0.05f;
|
||||
|
||||
// Sets default values
|
||||
AHandActor::AHandActor()
|
||||
{
|
||||
// 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;
|
||||
|
||||
}
|
||||
|
||||
// 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
|
||||
void AHandActor::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
DrawDebugCoordinateSystem(GetWorld(), FVector::ZeroVector, FRotator::ZeroRotator, 100.0f, false, 0.1f, 0, 2.0f);
|
||||
//SetActorLocation(GetPalmCenter());
|
||||
|
||||
if (Points.Num() != 21) return;
|
||||
for(int32 i = 0; i < HandBoneCount; ++i)
|
||||
{
|
||||
const int32 BoneA = HandBones[i][0];
|
||||
const int32 BoneB = HandBones[i][1];
|
||||
DrawDebugLine(
|
||||
GetWorld(),
|
||||
Points[BoneA],
|
||||
Points[BoneB],
|
||||
FColor::Red,
|
||||
false,
|
||||
Life,
|
||||
0,
|
||||
1.5f
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
for (const FVector& P : Points) {
|
||||
DrawDebugSphere(GetWorld(), P, 2.0f, 8, FColor::Green, false, Life);
|
||||
}
|
||||
}
|
||||
|
||||
FVector AHandActor::GetPalmCenter()
|
||||
{
|
||||
return ((Points[0] + Points[5] + Points[9] + Points[13] + Points[17]) / 5.0f);
|
||||
}
|
||||
|
||||
EHandedness AHandActor::GetHandedness()
|
||||
{
|
||||
return Handedness;
|
||||
}
|
||||
|
||||
void AHandActor::InitHand_Implementation(const FHandPacketHand& NewHand, AHandManager* NewHandManager, TSubclassOf<class AFingerTip> FingerTipClass)
|
||||
{
|
||||
Points = NewHand.Points;
|
||||
Handedness = NewHand.Handedness;
|
||||
Confidence = NewHand.Confidence;
|
||||
HandId = NewHand.HandId;
|
||||
HandManager = NewHandManager;
|
||||
|
||||
|
||||
//Spawn Finger Tips
|
||||
for (int32 FingerTipPoint : FingerTipBones)
|
||||
{
|
||||
FTransform SpawnTransform;
|
||||
SpawnTransform.SetLocation(GetPalmCenter());
|
||||
SpawnTransform.SetRotation(FQuat::Identity);
|
||||
SpawnTransform.SetScale3D(FVector::OneVector);
|
||||
|
||||
FActorSpawnParameters Params;
|
||||
Params.Owner = this;
|
||||
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
|
||||
AFingerTip* NewFinger = GetWorld()->SpawnActor<AFingerTip>(
|
||||
FingerTipClass,
|
||||
SpawnTransform,
|
||||
Params
|
||||
);
|
||||
|
||||
FingerTipActorList.Add(NewFinger);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void AHandActor::UpdateHand(const FHandPacketHand& NewHand)
|
||||
{
|
||||
GetWorld()->GetTimerManager().ClearTimer(DeathTimer);
|
||||
GetWorld()->GetTimerManager().SetTimer(DeathTimer, this, &AHandActor::HandDeath, DeathTime, true);
|
||||
|
||||
Points = NewHand.Points;
|
||||
Handedness = NewHand.Handedness;
|
||||
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]];
|
||||
FingerTipActorList[n]->SetActorLocation(f);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void AHandActor::HandDeath()
|
||||
{
|
||||
for (AFingerTip* FingerTip : FingerTipActorList) {
|
||||
FingerTip->Destroy();
|
||||
}
|
||||
|
||||
if (IsValid(HandManager))
|
||||
{
|
||||
HandManager->AddDeadHand(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#include "HandManager.h"
|
||||
#include "GestureManagerComponent.h"
|
||||
#include "HandActor.h"
|
||||
|
||||
// Sets default values
|
||||
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<UGestureManagerComponent>(TEXT("GestureManager"));
|
||||
}
|
||||
|
||||
// Called when the game starts or when spawned
|
||||
void AHandManager::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
if (!IsValid(HandActorClass))
|
||||
{
|
||||
HandActorClass = AHandActor::StaticClass();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Called every frame
|
||||
void AHandManager::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
|
||||
if (!IsValid(DM)) return;
|
||||
|
||||
RemoveDeadHands();
|
||||
UpdateHands();
|
||||
|
||||
GestureManager->EvaluateHands();
|
||||
}
|
||||
|
||||
void AHandManager::SpawnNewHand(const FVector& PalmCenter, const FHandPacketHand& NewHand)
|
||||
{
|
||||
FTransform SpawnTransform;
|
||||
SpawnTransform.SetLocation(PalmCenter);
|
||||
SpawnTransform.SetRotation(FQuat::Identity);
|
||||
SpawnTransform.SetScale3D(FVector::OneVector);
|
||||
|
||||
FActorSpawnParameters Params;
|
||||
Params.Owner = this;
|
||||
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
|
||||
AHandActor* NewHandActor = GetWorld()->SpawnActor<AHandActor>(
|
||||
HandActorClass,
|
||||
SpawnTransform,
|
||||
Params
|
||||
);
|
||||
|
||||
if (NewHandActor)
|
||||
{
|
||||
HandList.Add(NewHandActor);
|
||||
NewHandActor->InitHand(NewHand, this, FingerTipClass);
|
||||
}
|
||||
|
||||
OnHandAdded.Broadcast(NewHandActor);
|
||||
}
|
||||
|
||||
void AHandManager::AddDeadHand(AHandActor* DeadHand)
|
||||
{
|
||||
DeadHands.Add(DeadHand);
|
||||
}
|
||||
|
||||
TArray<AHandActor*> AHandManager::GetHandList()
|
||||
{
|
||||
return HandList;
|
||||
}
|
||||
|
||||
void AHandManager::RemoveDeadHands()
|
||||
{
|
||||
for (AHandActor* Hand : DeadHands)
|
||||
{
|
||||
if (IsValid(Hand))
|
||||
{
|
||||
HandList.Remove(Hand);
|
||||
OnHandRemoved.Broadcast(Hand);
|
||||
Hand->Destroy();
|
||||
}
|
||||
}
|
||||
DeadHands.Empty();
|
||||
}
|
||||
|
||||
void AHandManager::UpdateHands()
|
||||
{
|
||||
TArray<FHandPacketHand> NewHands;
|
||||
int32 OutSeq;
|
||||
int64 OutTimestampMs;
|
||||
|
||||
if (DM->ConsumeLatestHands(NewHands, OutSeq, OutTimestampMs)) {
|
||||
bool HandFound;
|
||||
|
||||
for (const FHandPacketHand& NewHand : NewHands)
|
||||
{
|
||||
const TArray<FVector>& Points = NewHand.Points;
|
||||
HandFound = false;
|
||||
|
||||
if (Points.Num() < 21) continue;
|
||||
const FVector PalmCenter = (Points[0] + Points[5] + Points[9] + Points[13] + Points[17]) / 5.0f;
|
||||
|
||||
for (AHandActor* Hand : HandList)
|
||||
{
|
||||
if (!IsValid(Hand)) continue;
|
||||
if (Hand->GetHandedness() != NewHand.Handedness) continue;
|
||||
|
||||
float DistSq = FVector::DistSquared(Hand->GetPalmCenter(), PalmCenter);
|
||||
|
||||
if (DistSq > HandAllocationDistance) continue;
|
||||
|
||||
Hand->UpdateHand(NewHand);
|
||||
HandFound = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!HandFound)
|
||||
{
|
||||
SpawnNewHand(PalmCenter, NewHand);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "JsonSelectionOption.h"
|
||||
|
||||
FString UJsonSelectionOption::GetOptionDisplayName()
|
||||
{
|
||||
// Gets the folder containing the json, e.g. "Tyranid"
|
||||
FString Folder = FPaths::GetCleanFilename(FPaths::GetPath(OptionValue));
|
||||
return Folder;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "UserHUDWidget.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "HAL/FileManager.h"
|
||||
|
||||
|
||||
|
||||
TArray<FString> UUserHUDWidget::GetJsonFileNames()
|
||||
{
|
||||
TArray<FString> FileNames;
|
||||
|
||||
FString FolderPath = ConfigFolderPath;
|
||||
|
||||
IFileManager::Get().FindFilesRecursive(
|
||||
FileNames,
|
||||
*FolderPath,
|
||||
TEXT("*.json"),
|
||||
true, // files
|
||||
false // directories
|
||||
);
|
||||
|
||||
//for (FString& Name : FileNames) FPaths::GetBaseFilename(Name);
|
||||
|
||||
return FileNames;
|
||||
}
|
||||
|
||||
UContentWidgetData* UUserHUDWidget::LoadUserHUDJsonFile(const FString& FullPath)
|
||||
{
|
||||
FString JsonString;
|
||||
|
||||
// Load the File as a String
|
||||
if (!FFileHelper::LoadFileToString(JsonString, *FullPath))
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("LoadUserHUDJsonFile: failed to read %s"), *FullPath);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> RootObject;
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);
|
||||
|
||||
//Convert the Loaded String to a JSON Format
|
||||
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("LoadUserHUDJsonFile: failed to parse JSON at %s"), *FullPath);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return ParseContentData(RootObject, this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
UContentWidgetData* UUserHUDWidget::ParseContentData(TSharedPtr<FJsonObject> JsonObject, UObject* Outer)
|
||||
{
|
||||
UContentWidgetData* ContentData = NewObject<UContentWidgetData>(Outer);
|
||||
|
||||
// WidgetType
|
||||
JsonObject->TryGetStringField(ContentJsonKeys::Type, ContentData->WidgetType);
|
||||
|
||||
// Sources array
|
||||
const TArray<TSharedPtr<FJsonValue>>* SourcesArray;
|
||||
if (JsonObject->TryGetArrayField(ContentJsonKeys::Sources, SourcesArray))
|
||||
{
|
||||
for (const TSharedPtr<FJsonValue>& SourceValue : *SourcesArray)
|
||||
{
|
||||
TSharedPtr<FJsonObject> SourceObj = SourceValue->AsObject();
|
||||
if (SourceObj.IsValid())
|
||||
ContentData->DataSources.Add(ParseContentSource(SourceObj));
|
||||
}
|
||||
}
|
||||
|
||||
// Children array — [{ "name": "Rules", "content": { ... } }, ...]
|
||||
const TArray<TSharedPtr<FJsonValue>>* ChildrenArray;
|
||||
if (JsonObject->TryGetArrayField(ContentJsonKeys::SubWidgets, ChildrenArray))
|
||||
{
|
||||
for (const TSharedPtr<FJsonValue>& ChildValue : *ChildrenArray)
|
||||
{
|
||||
TSharedPtr<FJsonObject> SubWidgetObj = ChildValue->AsObject();
|
||||
if (!SubWidgetObj.IsValid()) continue;
|
||||
|
||||
FString SubWidgetName;
|
||||
SubWidgetObj->TryGetStringField(ContentJsonKeys::Name, SubWidgetName);
|
||||
|
||||
const TSharedPtr<FJsonObject>* ContentObj;
|
||||
if (SubWidgetObj->TryGetObjectField(ContentJsonKeys::Content, ContentObj))
|
||||
{
|
||||
ContentData->SubWidgetNames.Add(SubWidgetName);
|
||||
// Recurse — pass Node as Outer so GC hierarchy is correct
|
||||
ContentData->SubWidgets.Add(ParseContentData(*ContentObj, ContentData));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ContentData;
|
||||
}
|
||||
|
||||
FContentSource UUserHUDWidget::ParseContentSource(TSharedPtr<FJsonObject> SourceObject)
|
||||
{
|
||||
FContentSource Source;
|
||||
|
||||
FString ContentTypeString;
|
||||
SourceObject->TryGetStringField(ContentJsonKeys::ContentType, ContentTypeString);
|
||||
SourceObject->TryGetStringField(ContentJsonKeys::Source, Source.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 Source.ContentType = EContentSourceType::InlineText;
|
||||
|
||||
return Source;
|
||||
}
|
||||
Reference in New Issue
Block a user