Backend: - Add /api/v1/ue/* endpoints (compute-points, cables, landing-points, satellites, status) returning flat JSON optimised for UE5 C++ parsing UE5 client (ue_client/): - PlanetDataManager: HTTP fetch + local mock JSON loader, spawns ComputePointActors - ComputePointActor / InteractiveObjectBase: hover/select state, material switching - GlobeInteractionComponent: drag-to-rotate via CesiumGeoreference origin shift, inertia, zoom - StereoRenderingManager: runtime SbS/TbB stereo toggle, IPD control (format TBD) - MotionCaptureInterface: protocol-agnostic gesture/rotate/zoom delegate interface (impl TBD) - PlanetPlayerController: unified mouse + motion-capture input routing - PlanetGameMode, Build.cs, Config, mock data Docs: - ue5_mvp_fused_plan.md updated to v3.0 for LED display context - ue_client_setup_guide.md: step-by-step editor setup guide - ue_todo.md: pending items blocked on vendor answers (stereo format + mocap protocol) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
221 lines
7.1 KiB
C++
221 lines
7.1 KiB
C++
#include "PlanetDataManager.h"
|
|
#include "ComputePointActor.h"
|
|
|
|
#include "HttpModule.h"
|
|
#include "Interfaces/IHttpResponse.h"
|
|
#include "Dom/JsonObject.h"
|
|
#include "Dom/JsonValue.h"
|
|
#include "Serialization/JsonReader.h"
|
|
#include "Serialization/JsonSerializer.h"
|
|
#include "Misc/FileHelper.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Engine/World.h"
|
|
|
|
// Cesium coordinate conversion
|
|
#include "CesiumGeoreference.h"
|
|
|
|
APlanetDataManager::APlanetDataManager()
|
|
{
|
|
PrimaryActorTick.bCanEverTick = false;
|
|
}
|
|
|
|
void APlanetDataManager::BeginPlay()
|
|
{
|
|
Super::BeginPlay();
|
|
|
|
// Build default mock path if not overridden
|
|
if (MockDataPath.IsEmpty())
|
|
{
|
|
MockDataPath = FPaths::ProjectContentDir() / TEXT("Data/mock_compute_points.json");
|
|
}
|
|
|
|
FetchAllData();
|
|
}
|
|
|
|
void APlanetDataManager::FetchAllData()
|
|
{
|
|
// Clear previous actors
|
|
for (AComputePointActor* Point : SpawnedPoints)
|
|
{
|
|
if (IsValid(Point)) Point->Destroy();
|
|
}
|
|
SpawnedPoints.Empty();
|
|
|
|
if (bUseLocalMockData)
|
|
{
|
|
LoadMockComputePoints();
|
|
}
|
|
else
|
|
{
|
|
FetchComputePoints();
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Phase A: local JSON file
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void APlanetDataManager::LoadMockComputePoints()
|
|
{
|
|
FString JsonStr;
|
|
if (!FFileHelper::LoadFileToString(JsonStr, *MockDataPath))
|
|
{
|
|
UE_LOG(LogTemp, Error, TEXT("PlanetDataManager: cannot read mock file: %s"), *MockDataPath);
|
|
return;
|
|
}
|
|
TArray<FComputePoint> Points = ParseComputePointsJson(JsonStr);
|
|
UE_LOG(LogTemp, Log, TEXT("PlanetDataManager: loaded %d points from mock file"), Points.Num());
|
|
SpawnComputePoints(Points);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Phase B: HTTP request
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void APlanetDataManager::FetchComputePoints()
|
|
{
|
|
const FString Url = BackendBaseUrl + TEXT("/api/v1/ue/compute-points");
|
|
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Req = FHttpModule::Get().CreateRequest();
|
|
Req->SetURL(Url);
|
|
Req->SetVerb(TEXT("GET"));
|
|
Req->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
|
|
Req->OnProcessRequestComplete().BindUObject(
|
|
this, &APlanetDataManager::OnComputePointsResponse);
|
|
Req->ProcessRequest();
|
|
UE_LOG(LogTemp, Log, TEXT("PlanetDataManager: GET %s"), *Url);
|
|
}
|
|
|
|
void APlanetDataManager::OnComputePointsResponse(FHttpRequestPtr Request,
|
|
FHttpResponsePtr Response,
|
|
bool bSuccess)
|
|
{
|
|
if (!bSuccess || !Response.IsValid())
|
|
{
|
|
UE_LOG(LogTemp, Error,
|
|
TEXT("PlanetDataManager: HTTP request failed. Is the backend running at %s?"),
|
|
*BackendBaseUrl);
|
|
return;
|
|
}
|
|
if (Response->GetResponseCode() != 200)
|
|
{
|
|
UE_LOG(LogTemp, Error, TEXT("PlanetDataManager: HTTP %d from %s"),
|
|
Response->GetResponseCode(), *Request->GetURL());
|
|
return;
|
|
}
|
|
|
|
TArray<FComputePoint> Points = ParseComputePointsJson(Response->GetContentAsString());
|
|
UE_LOG(LogTemp, Log, TEXT("PlanetDataManager: received %d compute points"), Points.Num());
|
|
SpawnComputePoints(Points);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// JSON parser
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TArray<FComputePoint> APlanetDataManager::ParseComputePointsJson(const FString& JsonStr)
|
|
{
|
|
TArray<FComputePoint> Result;
|
|
|
|
TSharedPtr<FJsonObject> Root;
|
|
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonStr);
|
|
if (!FJsonSerializer::Deserialize(Reader, Root) || !Root.IsValid())
|
|
{
|
|
UE_LOG(LogTemp, Error, TEXT("PlanetDataManager: JSON parse failed"));
|
|
return Result;
|
|
}
|
|
|
|
const TArray<TSharedPtr<FJsonValue>>* Items;
|
|
if (!Root->TryGetArrayField(TEXT("items"), Items))
|
|
{
|
|
UE_LOG(LogTemp, Error, TEXT("PlanetDataManager: no 'items' array in JSON"));
|
|
return Result;
|
|
}
|
|
|
|
for (const TSharedPtr<FJsonValue>& Val : *Items)
|
|
{
|
|
const TSharedPtr<FJsonObject>* ObjPtr;
|
|
if (!Val->TryGetObject(ObjPtr)) continue;
|
|
const TSharedPtr<FJsonObject>& Obj = *ObjPtr;
|
|
|
|
FComputePoint Pt;
|
|
Obj->TryGetStringField(TEXT("id"), Pt.Id);
|
|
Obj->TryGetStringField(TEXT("name"), Pt.Name);
|
|
Obj->TryGetStringField(TEXT("country"), Pt.Country);
|
|
Obj->TryGetStringField(TEXT("city"), Pt.City);
|
|
|
|
double Lat, Lon;
|
|
if (!Obj->TryGetNumberField(TEXT("latitude"), Lat)) continue;
|
|
if (!Obj->TryGetNumberField(TEXT("longitude"), Lon)) continue;
|
|
Pt.Latitude = (float)Lat;
|
|
Pt.Longitude = (float)Lon;
|
|
|
|
int32 Rank;
|
|
if (Obj->TryGetNumberField(TEXT("rank"), Rank)) Pt.Rank = Rank;
|
|
|
|
double Rmax;
|
|
if (Obj->TryGetNumberField(TEXT("rmax_tflops"), Rmax)) Pt.RmaxTFlops = (float)Rmax;
|
|
double Rpeak;
|
|
if (Obj->TryGetNumberField(TEXT("rpeak_tflops"), Rpeak)) Pt.RpeakTFlops = (float)Rpeak;
|
|
int32 Cores;
|
|
if (Obj->TryGetNumberField(TEXT("cores"), Cores)) Pt.Cores = Cores;
|
|
double Power;
|
|
if (Obj->TryGetNumberField(TEXT("power_kw"), Power)) Pt.PowerKw = (float)Power;
|
|
|
|
Result.Add(Pt);
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Spawn actors
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void APlanetDataManager::SpawnComputePoints(const TArray<FComputePoint>& Points)
|
|
{
|
|
if (!ComputePointClass)
|
|
{
|
|
UE_LOG(LogTemp, Warning,
|
|
TEXT("PlanetDataManager: ComputePointClass not set — set it in the Details Panel"));
|
|
return;
|
|
}
|
|
|
|
UWorld* World = GetWorld();
|
|
if (!World) return;
|
|
|
|
// Find the CesiumGeoreference in the level
|
|
ACesiumGeoreference* Georeference = ACesiumGeoreference::GetDefaultGeoreference(World);
|
|
if (!Georeference)
|
|
{
|
|
UE_LOG(LogTemp, Error,
|
|
TEXT("PlanetDataManager: no CesiumGeoreference in level. Add a CesiumGeoreference actor."));
|
|
return;
|
|
}
|
|
|
|
for (const FComputePoint& Pt : Points)
|
|
{
|
|
// Convert geographic coordinates to Unreal world coordinates
|
|
// Altitude is PointAltitudeMeters above sea level
|
|
FVector WorldPos = Georeference->TransformLongitudeLatitudeHeightPositionToUnreal(
|
|
FVector(Pt.Longitude, Pt.Latitude, PointAltitudeMeters)
|
|
);
|
|
|
|
FActorSpawnParameters Params;
|
|
Params.SpawnCollisionHandlingOverride =
|
|
ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
|
|
|
AComputePointActor* Actor = World->SpawnActor<AComputePointActor>(
|
|
ComputePointClass, WorldPos, FRotator::ZeroRotator, Params);
|
|
|
|
if (Actor)
|
|
{
|
|
Actor->SetPointData(Pt);
|
|
SpawnedPoints.Add(Actor);
|
|
}
|
|
}
|
|
|
|
OnComputePointsLoaded.Broadcast(SpawnedPoints.Num());
|
|
UE_LOG(LogTemp, Log, TEXT("PlanetDataManager: spawned %d compute point actors"),
|
|
SpawnedPoints.Num());
|
|
}
|