Files
planet/ue_client/Source/PlanetClient/PlanetDataManager.cpp
linkong 1e6f4b338b feat: add UE5 LED display client — backend API, C++ source, stereo framework
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>
2026-04-14 18:39:05 +08:00

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());
}