first commit

This commit is contained in:
DuOtto
2026-04-28 13:17:01 +02:00
commit c7c37b339c
106 changed files with 6310 additions and 0 deletions
@@ -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;
}
+27
View File
@@ -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)
{
}
+148
View File
@@ -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);
}
}
+128
View File
@@ -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;
}