LifePlay-Revival/Source/LifePlayEditorTools/LPBuildCitySidewalksCommandlet.cpp
2026-10-22 21:10:39 +02:00

364 lines
18 KiB
C++

// Lot STM1 (STM1-SPEC-17, option T ; pack 1.4.0 ; STM1-DIAG-03 E1 : 1.4.1). Voir LPBuildCitySidewalksCommandlet.h.
#include "LPBuildCitySidewalksCommandlet.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "CompGeom/PolygonTriangulation.h"
#include "Engine/StaticMesh.h"
#include "HAL/FileManager.h"
#include "IndexTypes.h"
#include "LPCityOsm.h"
#include "Materials/MaterialInterface.h"
#include "Misc/FileHelper.h"
#include "Misc/PackageName.h"
#include "RawMesh.h"
#include "StreetMap.h"
#include "UObject/Package.h"
#include "UObject/SavePackage.h"
DEFINE_LOG_CATEGORY_STATIC(LogLPBuildCitySidewalks, Log, All);
namespace LPBuildCitySidewalks
{
bool IsAllowedPackage(const FString& PackageName)
{
TArray<FString> Parts;
PackageName.ParseIntoArray(Parts, TEXT("/"));
return Parts.Num() == 4 && Parts[0].StartsWith(TEXT("lpm_city_")) && Parts[1] == TEXT("StreetMap") && Parts[2] == TEXT("Sidewalks");
}
enum ELayer : int32 { Sidewalk = 0, Stone = 1, Green = 2, Slab = 3, Light = 4, Sand = 5, Water = 6 };
/** 1.5.2 (STM1-DIAG-06 § 3 ter) : couche d'une surface pietonne (surface fermee ou rue pietonne) selon sa balise surface :
* paves (sett, cobblestone) -> pierre ; asphalt -> beton clair ; terre battue et graviers -> stabilise ; autre ou sans
* balise -> dalles. */
ELayer SurfaceLayer(const TMap<FString, FString>& Tags)
{
const FString Surface = Tags.FindRef(TEXT("surface"));
// 1.7.1 (STM1-DIAG-10 D) : places en boucle (lp:source P3, sans balise surface) en pierre, comme en 1.5.1 (Vendome).
if (Surface.IsEmpty() && Tags.FindRef(TEXT("lp:source")).StartsWith(TEXT("P3"))) return Stone;
if (Surface == TEXT("sett") || Surface == TEXT("cobblestone") || Surface == TEXT("unhewn_cobblestone")) return Stone;
if (Surface == TEXT("asphalt")) return Light;
for (const TCHAR* Loose : { TEXT("compacted"), TEXT("gravel"), TEXT("fine_gravel"), TEXT("unpaved"), TEXT("ground"), TEXT("dirt"),
TEXT("earth"), TEXT("sand"), TEXT("pebblestone") })
{
if (Surface == Loose) return Sand;
}
return Slab;
}
/** 1.5.1 (STM1-SPEC-21) : surface de pierre en dalles plutot qu'en paves : cours de monuments (lp:source P4), parvis,
* esplanades, cours et terrasses (nom), dalles explicites (surface stone, concrete:plates, marble, granite). */
bool IsSlab(const TMap<FString, FString>& Tags)
{
if (Tags.FindRef(TEXT("lp:source")).StartsWith(TEXT("P4"))) return true;
const FString Name = Tags.FindRef(TEXT("name"));
for (const TCHAR* Prefix : { TEXT("Parvis"), TEXT("Esplanade"), TEXT("Cour "), TEXT("Cour d"), TEXT("Terrasse") })
{
if (Name.StartsWith(Prefix, ESearchCase::CaseSensitive)) return true;
}
const FString Surface = Tags.FindRef(TEXT("surface"));
return Surface == TEXT("stone") || Surface == TEXT("concrete:plates") || Surface == TEXT("marble") || Surface == TEXT("granite");
}
/** 1.7.6 (STM1-DIAG-16 § 3, W) : plan d'eau ferme qui n'est pas le fleuve (bassins, fontaines, etangs). */
bool IsStillWater(const TMap<FString, FString>& Tags)
{
if (Tags.FindRef(TEXT("water")) == TEXT("river") || Tags.FindRef(TEXT("waterway")) == TEXT("riverbank")) return false;
return Tags.FindRef(TEXT("natural")) == TEXT("water") || Tags.FindRef(TEXT("amenity")) == TEXT("fountain") || Tags.Contains(TEXT("water"));
}
ELayer AreaLayer(const TMap<FString, FString>& Tags, bool& bOut, bool bWater)
{
bOut = true;
if (bWater && IsStillWater(Tags)) return Water;
const FString Highway = Tags.FindRef(TEXT("highway"));
if ((Highway == TEXT("pedestrian") || Highway == TEXT("footway")) && Tags.FindRef(TEXT("area")) == TEXT("yes")) return Stone;
if (Tags.FindRef(TEXT("place")) == TEXT("square") || Tags.Contains(TEXT("area:highway"))) return Stone;
const FString Landuse = Tags.FindRef(TEXT("landuse"));
if (Tags.FindRef(TEXT("leisure")) == TEXT("garden") || Landuse == TEXT("grass") || Landuse == TEXT("flowerbed")
|| Landuse == TEXT("village_green") || Landuse == TEXT("recreation_ground")) return Green;
bOut = false;
return Sidewalk;
}
struct FBuilder
{
FRawMesh Mesh;
int32 Triangles[7] = { 0, 0, 0, 0, 0, 0, 0 };
/** Triangle face vers le haut (+Z) : aire signee negative en XY, comme AddThick2DLine du plugin. */
void AddTriangle(FVector2f A, FVector2f B, FVector2f C, float Z, int32 Slot)
{
const float Cross = (B.X - A.X) * (C.Y - A.Y) - (B.Y - A.Y) * (C.X - A.X);
if (FMath::Abs(Cross) < 1.f) return;
if (Cross > 0.f) Swap(B, C);
const FVector2f Corners[3] = { A, B, C };
const int32 First = Mesh.VertexPositions.Num();
for (int32 I = 0; I < 3; ++I)
{
Mesh.VertexPositions.Add(FVector3f(Corners[I], Z));
Mesh.WedgeIndices.Add(First + I);
// Base tangente alignee sur X local, comme l'echantillonnage des textures (position locale XY).
Mesh.WedgeTangentX.Add(FVector3f(1.f, 0.f, 0.f));
Mesh.WedgeTangentY.Add(FVector3f(0.f, 1.f, 0.f));
Mesh.WedgeTangentZ.Add(FVector3f::UpVector);
Mesh.WedgeTexCoords[0].Add(Corners[I] / 300.f);
Mesh.WedgeColors.Add(FColor::White);
}
Mesh.FaceMaterialIndices.Add(Slot);
Mesh.FaceSmoothingMasks.Add(0);
++Triangles[Slot];
}
/** Bande d'un segment, prolongee d'une demi-largeur a chaque bout pour fermer les angles. */
void AddStrip(const FVector2f& A, const FVector2f& B, float Half, float Z, int32 Slot)
{
const float Len = FVector2f::Distance(A, B);
const FVector2f Dir = (B - A) / Len;
const FVector2f Right(-Dir.Y, Dir.X);
const FVector2f S = A - Dir * Half, E = B + Dir * Half;
AddTriangle(S - Right * Half, S + Right * Half, E + Right * Half, Z, Slot);
AddTriangle(S - Right * Half, E + Right * Half, E - Right * Half, Z, Slot);
}
};
}
ULPBuildCitySidewalksCommandlet::ULPBuildCitySidewalksCommandlet()
{
IsClient = false;
IsServer = false;
IsEditor = true;
LogToConsole = true;
}
int32 ULPBuildCitySidewalksCommandlet::Main(const FString& Params)
{
using namespace LPBuildCitySidewalks;
TArray<FString> Tokens, Switches;
TMap<FString, FString> ParamsMap;
ParseCommandLine(*Params, Tokens, Switches, ParamsMap);
const FString MapPath = ParamsMap.FindRef(TEXT("StreetMap"));
const FString OsmPath = ParamsMap.FindRef(TEXT("Osm"));
const FString OriginText = ParamsMap.FindRef(TEXT("Origin"));
const FString PackageName = ParamsMap.FindRef(TEXT("Package"));
const FString ReportPath = ParamsMap.FindRef(TEXT("Report"));
auto Number = [&ParamsMap](const TCHAR* Key, double Default) { return ParamsMap.Contains(Key) ? FCString::Atod(*ParamsMap[Key]) : Default; };
const double WidthCm = Number(TEXT("WidthM"), 2.5) * 100.0;
const double PedestrianWidthCm = Number(TEXT("PedestrianWidthM"), 6.0) * 100.0;
// 1.5.1 : -SlabMaterial ajoute la couche des dalles (sinon ces surfaces restent en pierre) ; -SlabZCm (pierre + 1 par defaut).
// 1.5.2 : -BySurface (avec -SlabMaterial, -LightMaterial, -SandMaterial) : couche de chaque surface pietonne selon sa balise
// surface (SurfaceLayer) ; -LightZCm et -SandZCm (pierre + 1 et + 0,5 par defaut).
const bool bBySurface = Switches.Contains(TEXT("BySurface"));
const bool bSlab = ParamsMap.Contains(TEXT("SlabMaterial"));
const float StoneZ = float(Number(TEXT("StoneZCm"), -3.0));
// 1.7.6 (W) : -WaterMaterial (avec -BySurface) ajoute la couche des plans d'eau fermes hors fleuve, a -WaterZCm (5 par defaut).
const bool bWater = ParamsMap.Contains(TEXT("WaterMaterial"));
const float Z[7] = { float(Number(TEXT("ZCm"), -5.0)), StoneZ, float(Number(TEXT("GreenZCm"), -1.0)), float(Number(TEXT("SlabZCm"), StoneZ + 1.0)),
float(Number(TEXT("LightZCm"), StoneZ + 1.0)), float(Number(TEXT("SandZCm"), StoneZ + 0.5)), float(Number(TEXT("WaterZCm"), 5.0)) };
const int32 NumSlots = bWater ? 7 : bBySurface ? 6 : bSlab ? 4 : 3;
const bool bAreas = !Switches.Contains(TEXT("NoAreas"));
const bool bDryRun = Switches.Contains(TEXT("DryRun"));
const TCHAR* const SlotNames[7] = { TEXT("Sidewalk"), TEXT("Stone"), TEXT("Green"), TEXT("Slab"), TEXT("Light"), TEXT("Sand"), TEXT("Water") };
const TCHAR* const MaterialKeys[7] = { TEXT("Material"), TEXT("StoneMaterial"), TEXT("GreenMaterial"), TEXT("SlabMaterial"), TEXT("LightMaterial"), TEXT("SandMaterial"), TEXT("WaterMaterial") };
TArray<FString> Report;
auto Line = [&Report](const FString& Text)
{
UE_LOG(LogLPBuildCitySidewalks, Display, TEXT("%s"), *Text);
Report.Add(Text);
};
auto Finish = [&](int32 Code)
{
Line(FString::Printf(TEXT("CODE %d"), Code));
if (!ReportPath.IsEmpty())
{
FFileHelper::SaveStringArrayToFile(Report, *ReportPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
}
return Code;
};
TArray<FString> OriginParts;
OriginText.ParseIntoArray(OriginParts, TEXT(","));
if (MapPath.IsEmpty() || OsmPath.IsEmpty() || PackageName.IsEmpty() || OriginParts.Num() != 2)
{
Line(TEXT("usage : -StreetMap=<objet> -Osm=<f.osm.xml> -Origin=<lat>,<lon> -Package=</lpm_city_<v>/StreetMap/Sidewalks/<Ville>> [-Material=<objet>] [-StoneMaterial=<objet>] [-GreenMaterial=<objet>] [-SlabMaterial=<objet>] [-BySurface -LightMaterial=<objet> -SandMaterial=<objet> [-LightZCm] [-SandZCm]] [-WidthM=2.5] [-PedestrianWidthM=6] [-ZCm=-5] [-StoneZCm=-3] [-GreenZCm=-1] [-SlabZCm=<pierre+1>] [-WaterMaterial=<objet> [-WaterZCm=5]] [-NoAreas] [-DryRun] [-Report=<f.md>]"));
return Finish(2);
}
Line(FString::Printf(TEXT("# LPBuildCitySidewalks %s <- %s (%s) ; trottoirs %.2f m a %.1f cm ; pierre a %.1f cm (rues pietonnes %.1f m) ; vert a %.1f cm%s%s"), *PackageName, *OsmPath,
bDryRun ? TEXT("DryRun") : TEXT("ecriture"), WidthCm / 100.0, Z[0], Z[1], PedestrianWidthCm / 100.0, Z[2], bAreas ? TEXT("") : TEXT(" ; surfaces non traitees (-NoAreas)"),
bSlab ? *FString::Printf(TEXT(" ; dalles a %.1f cm%s"), Z[3], bBySurface ? *FString::Printf(TEXT(" ; selon surface : beton clair a %.1f cm, stabilise a %.1f cm"), Z[4], Z[5]) : TEXT("")) : TEXT("")));
if (bWater && !bBySurface)
{
Line(TEXT("ECHEC : -WaterMaterial demande -BySurface ; aucun asset ecrit"));
return Finish(2);
}
if (bBySurface && !(bSlab && ParamsMap.Contains(TEXT("LightMaterial")) && ParamsMap.Contains(TEXT("SandMaterial"))))
{
Line(TEXT("ECHEC : -BySurface demande -SlabMaterial, -LightMaterial et -SandMaterial ; aucun asset ecrit"));
return Finish(2);
}
if (!IsAllowedPackage(PackageName) || !FPackageName::IsValidLongPackageName(PackageName))
{
Line(TEXT("ECHEC : paquet hors de /lpm_city_*/StreetMap/Sidewalks/ ; aucun asset ecrit"));
return Finish(1);
}
if (FPackageName::DoesPackageExist(PackageName))
{
Line(FString::Printf(TEXT("ECHEC : %s existe deja ; aucun asset ecrit"), *PackageName));
return Finish(1);
}
const UStreetMap* Map = LoadObject<UStreetMap>(nullptr, *MapPath);
UMaterialInterface* Materials[7] = { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr };
for (int32 S = 0; S < NumSlots; ++S)
{
const FString Path = ParamsMap.FindRef(MaterialKeys[S]);
Materials[S] = Path.IsEmpty() ? nullptr : LoadObject<UMaterialInterface>(nullptr, *Path);
if (!Path.IsEmpty() && !Materials[S])
{
Line(FString::Printf(TEXT("ECHEC : materiau %s introuvable ; aucun asset ecrit"), *Path));
return Finish(1);
}
}
if (!Map)
{
Line(TEXT("ECHEC : carte introuvable ; aucun asset ecrit"));
return Finish(1);
}
// Boite de la carte : routes et batiments.
FBox2D Bounds(ForceInit);
for (const FStreetMapRoad& Road : Map->GetRoads()) for (const FVector2D& P : Road.RoadPoints) Bounds += P;
for (const FStreetMapBuilding& Building : Map->GetBuildings()) for (const FVector2D& P : Building.BuildingPoints) Bounds += P;
LPCityOsm::FReader Osm;
Osm.OriginLat = FCString::Atod(*OriginParts[0]);
Osm.OriginLon = FCString::Atod(*OriginParts[1]);
Osm.bLines = true;
Osm.bAreas = bAreas;
Osm.bWaterAreas = bWater;
FString Error;
if (!LPCityOsm::Read(Osm, OsmPath, Error))
{
Line(FString::Printf(TEXT("ECHEC : %s ; aucun asset ecrit"), *Error));
return Finish(1);
}
Osm.Nodes.Empty();
FBuilder Builder;
int32 SidewalkWays = 0, PedestrianWays = 0, Outside = 0, Areas[7] = { 0, 0, 0, 0, 0, 0, 0 }, FailedAreas = 0;
double LengthCm[7] = { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, AreaCm2[7] = { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 };
for (int32 L = 0; L < Osm.Lines.Num(); ++L)
{
const TMap<FString, FString>& Tags = Osm.LineTags[L];
const TArray<FVector2D>& Pts = Osm.Lines[L];
const FString Highway = Tags.FindRef(TEXT("highway"));
bool bIsArea = false;
ELayer Layer = bAreas ? AreaLayer(Tags, bIsArea, bWater) : Sidewalk;
if (bIsArea && bBySurface && Layer == Stone) Layer = SurfaceLayer(Tags);
else if (bIsArea && bSlab && Layer == Stone && IsSlab(Tags)) Layer = Slab;
if (bIsArea)
{
// Surface fermee : triangulation (dernier point = premier, retire).
TArray<FVector2d> Poly;
for (int32 P = 0; P + 1 < Pts.Num(); ++P) Poly.Add(FVector2d(Pts[P].X, Pts[P].Y));
if (Poly.Num() < 3 || !Bounds.IsInside(LPCityOsm::BoxCenter(Pts))) { ++Outside; continue; }
TArray<UE::Geometry::FIndex3i> Tris;
PolygonTriangulation::TriangulateSimplePolygon<double>(Poly, Tris, false);
if (Tris.IsEmpty()) { ++FailedAreas; continue; }
double Area = 0.0;
for (int32 P = 0; P < Poly.Num(); ++P) Area += Poly[P].X * Poly[(P + 1) % Poly.Num()].Y - Poly[(P + 1) % Poly.Num()].X * Poly[P].Y;
for (const UE::Geometry::FIndex3i& T : Tris)
{
Builder.AddTriangle(FVector2f(Poly[T.A]), FVector2f(Poly[T.B]), FVector2f(Poly[T.C]), Z[Layer], Layer);
}
++Areas[Layer];
AreaCm2[Layer] += FMath::Abs(Area) * 0.5;
continue;
}
const bool bSidewalk = Highway == TEXT("footway") && Tags.FindRef(TEXT("footway")) == TEXT("sidewalk");
const bool bPedestrian = bAreas && Highway == TEXT("pedestrian") && Tags.FindRef(TEXT("area")) != TEXT("yes");
if (!bSidewalk && !bPedestrian) continue;
const int32 Slot = bSidewalk ? Sidewalk : bBySurface ? int32(SurfaceLayer(Tags)) : int32(Stone);
(bSidewalk ? SidewalkWays : PedestrianWays)++;
const float Half = float((bSidewalk ? WidthCm : PedestrianWidthCm) * 0.5);
for (int32 P = 0; P + 1 < Pts.Num(); ++P)
{
const FVector2f A(Pts[P]), B(Pts[P + 1]);
const float Len = FVector2f::Distance(A, B);
if (Len < 1.f) continue;
if (!Bounds.IsInside(FVector2D((A + B) * 0.5f))) { ++Outside; continue; }
Builder.AddStrip(A, B, Half, Z[Slot], Slot);
LengthCm[Slot] += Len;
}
}
Line(FString::Printf(TEXT("trottoirs (footway=sidewalk) %d voies, %.1f km ; %d triangles"), SidewalkWays, LengthCm[Sidewalk] / 1e5, Builder.Triangles[Sidewalk]));
Line(FString::Printf(TEXT("pierre : %d surfaces (%.3f km2) et %d rues pietonnes (%.1f km) ; %d triangles"), Areas[Stone], AreaCm2[Stone] / 1e10, PedestrianWays, LengthCm[Stone] / 1e5, Builder.Triangles[Stone]));
Line(FString::Printf(TEXT("vert : %d surfaces (%.3f km2) ; %d triangles"), Areas[Green], AreaCm2[Green] / 1e10, Builder.Triangles[Green]));
if (bSlab)
{
Line(FString::Printf(TEXT("dalles : %d surfaces (%.3f km2) ; %d triangles"), Areas[Slab], AreaCm2[Slab] / 1e10, Builder.Triangles[Slab]));
}
if (bBySurface)
{
Line(FString::Printf(TEXT("selon surface : rues pietonnes en bandes (km) paves %.1f, dalles %.1f, beton clair %.1f, stabilise %.1f"),
LengthCm[Stone] / 1e5, LengthCm[Slab] / 1e5, LengthCm[Light] / 1e5, LengthCm[Sand] / 1e5));
Line(FString::Printf(TEXT("beton clair (asphalt) : %d surfaces (%.3f km2) ; %d triangles"), Areas[Light], AreaCm2[Light] / 1e10, Builder.Triangles[Light]));
Line(FString::Printf(TEXT("stabilise (terre battue, graviers) : %d surfaces (%.3f km2) ; %d triangles"), Areas[Sand], AreaCm2[Sand] / 1e10, Builder.Triangles[Sand]));
}
if (bWater)
{
Line(FString::Printf(TEXT("eau (plans d'eau fermes hors fleuve) : %d surfaces (%.4f km2) a %.1f cm ; %d triangles"), Areas[Water], AreaCm2[Water] / 1e10, Z[Water], Builder.Triangles[Water]));
}
Line(FString::Printf(TEXT("hors de la carte %d ; surfaces non triangulees %d ; total %d triangles"), Outside, FailedAreas, Builder.Mesh.WedgeIndices.Num() / 3));
if (Builder.Mesh.WedgeIndices.IsEmpty())
{
Line(TEXT("ECHEC : aucun triangle ; aucun asset ecrit"));
return Finish(1);
}
if (bDryRun)
{
Line(TEXT("DryRun : aucun asset ecrit"));
return Finish(0);
}
UPackage* Package = CreatePackage(*PackageName);
UStaticMesh* StaticMesh = NewObject<UStaticMesh>(Package, *FPackageName::GetLongPackageAssetName(PackageName), RF_Public | RF_Standalone);
StaticMesh->InitResources();
StaticMesh->SetLightingGuid(FGuid::NewGuid());
FStaticMeshSourceModel& SourceModel = StaticMesh->AddSourceModel();
SourceModel.BuildSettings.bRecomputeNormals = false;
SourceModel.BuildSettings.bRecomputeTangents = false;
SourceModel.BuildSettings.bRemoveDegenerates = false;
SourceModel.BuildSettings.bUseFullPrecisionUVs = false;
// Composant Movable pose en jeu : pas de lightmap.
SourceModel.BuildSettings.bGenerateLightmapUVs = false;
SourceModel.RawMeshBulkData->SaveRawMesh(Builder.Mesh);
for (int32 S = 0; S < NumSlots; ++S)
{
StaticMesh->GetStaticMaterials().Add(FStaticMaterial(Materials[S], SlotNames[S]));
}
StaticMesh->SetImportVersion(EImportStaticMeshVersion::LastVersion);
StaticMesh->Build(/*bSilent=*/true);
StaticMesh->PostEditChange();
FAssetRegistryModule::AssetCreated(StaticMesh);
Line(FString::Printf(TEXT("Build : sommets rendus LOD0 %d ; sections %d ; materiaux %s, %s, %s%s%s"), StaticMesh->GetNumVertices(0),
StaticMesh->GetNumSections(0), *GetPathNameSafe(Materials[0]), *GetPathNameSafe(Materials[1]), *GetPathNameSafe(Materials[2]),
bSlab ? TEXT(", ") : TEXT(""), bSlab ? *GetPathNameSafe(Materials[3]) : TEXT("")));
if (bBySurface)
{
Line(FString::Printf(TEXT("materiaux selon surface : %s, %s"), *GetPathNameSafe(Materials[4]), *GetPathNameSafe(Materials[5])));
}
FSavePackageArgs SaveArgs;
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
const FString FileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());
if (!UPackage::SavePackage(Package, StaticMesh, *FileName, SaveArgs))
{
Line(FString::Printf(TEXT("ECHEC : sauvegarde de %s"), *FileName));
return Finish(1);
}
Line(FString::Printf(TEXT("asset ecrit : %s (%s), %lld octets"), *PackageName, *FileName, IFileManager::Get().FileSize(*FileName)));
return Finish(0);
}