LifePlay-Revival/Source/LifePlayEditorTools/LPBuildCityWaterCommandlet.cpp
edujiro5972 56dfcfc3d2 LifePlay UE 5.8 : état de développement au 2026-10-20
Tickets : PUB-20261020-T1

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 17:35:10 +02:00

731 lines
31 KiB
C++

// Lot STM1 (STM1-SPEC-03, pilote O-B ; STM1-IMPL-02). Voir LPBuildCityWaterCommandlet.h.
#include "LPBuildCityWaterCommandlet.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "CompGeom/PolygonTriangulation.h"
#include "ConstrainedDelaunay2.h"
#include "Curve/GeneralPolygon2.h"
#include "LPCityWaterMask.h"
#include "Engine/StaticMesh.h"
#include "Materials/MaterialInterface.h"
#include "MeshDescription.h"
#include "Misc/FileHelper.h"
#include "Misc/PackageName.h"
#include "Polygon2.h"
#include "StaticMeshAttributes.h"
#include "StreetMap.h"
#include "UObject/Package.h"
#include "UObject/SavePackage.h"
#include "XmlFile.h"
#include "MaterialEditingLibrary.h"
#include "Materials/Material.h"
#include "Materials/MaterialExpressionConstant.h"
#include "Materials/MaterialExpressionConstant3Vector.h"
DEFINE_LOG_CATEGORY_STATIC(LogLPBuildCityWater, Log, All);
using namespace UE::Geometry;
namespace LPCityWater
{
// Meme constante que le plugin (StreetMapFactory.cpp:8-9), en centimetres par degre.
constexpr double CmPerDegree = 40075036.0 / 360.0 * 100.0;
/** Point OSM : X = longitude, Y = latitude. */
using FLonLat = FVector2d;
struct FOsmWay
{
int64 Id = 0;
TArray<FLonLat> Points;
TMap<FString, FString> Tags;
// STM1-B2 (R1) : centre de <bounds>. Une sortie Overpass "out tags geom" ne donne que la boite, pas les noeuds.
FLonLat BoundsCenter = FLonLat::ZeroVector;
bool bHasBounds = false;
};
struct FOsmMember
{
FString Role;
TArray<FLonLat> Points;
};
struct FOsmRelation
{
int64 Id = 0;
TArray<FOsmMember> Members;
TMap<FString, FString> Tags;
};
/** Points <nd lat=.. lon=..> d'un noeud XML (voie ou membre). */
void ReadPoints(const FXmlNode* Node, TArray<FLonLat>& Out)
{
for (const FXmlNode* Child : Node->GetChildrenNodes())
{
if (Child->GetTag() == TEXT("nd"))
{
const FString Lat = Child->GetAttribute(TEXT("lat"));
const FString Lon = Child->GetAttribute(TEXT("lon"));
if (!Lat.IsEmpty() && !Lon.IsEmpty())
{
Out.Add(FLonLat(FCString::Atod(*Lon), FCString::Atod(*Lat)));
}
}
}
}
void ReadTags(const FXmlNode* Node, TMap<FString, FString>& Out)
{
for (const FXmlNode* Child : Node->GetChildrenNodes())
{
if (Child->GetTag() == TEXT("tag"))
{
Out.Add(Child->GetAttribute(TEXT("k")), Child->GetAttribute(TEXT("v")));
}
}
}
/** Lit une reponse Overpass "out geom" (voies et relations avec geometrie en ligne). Echec : false et message. */
bool ReadOverpass(const FString& Path, TArray<FOsmWay>& Ways, TArray<FOsmRelation>& Relations, FString& Error)
{
FXmlFile File(Path);
if (!File.IsValid())
{
Error = FString::Printf(TEXT("XML illisible : %s (%s)"), *Path, *File.GetLastError());
return false;
}
const FXmlNode* Root = File.GetRootNode();
for (const FXmlNode* Node : Root->GetChildrenNodes())
{
if (Node->GetTag() == TEXT("remark"))
{
Error = FString::Printf(TEXT("reponse Overpass en erreur : %s"), *Node->GetContent());
return false;
}
if (Node->GetTag() == TEXT("way"))
{
FOsmWay& Way = Ways.AddDefaulted_GetRef();
Way.Id = FCString::Atoi64(*Node->GetAttribute(TEXT("id")));
ReadPoints(Node, Way.Points);
ReadTags(Node, Way.Tags);
if (const FXmlNode* Bounds = Node->FindChildNode(TEXT("bounds")))
{
const double MinLat = FCString::Atod(*Bounds->GetAttribute(TEXT("minlat")));
const double MinLon = FCString::Atod(*Bounds->GetAttribute(TEXT("minlon")));
const double MaxLat = FCString::Atod(*Bounds->GetAttribute(TEXT("maxlat")));
const double MaxLon = FCString::Atod(*Bounds->GetAttribute(TEXT("maxlon")));
Way.BoundsCenter = FLonLat(0.5 * (MinLon + MaxLon), 0.5 * (MinLat + MaxLat));
Way.bHasBounds = true;
}
}
else if (Node->GetTag() == TEXT("relation"))
{
FOsmRelation& Rel = Relations.AddDefaulted_GetRef();
Rel.Id = FCString::Atoi64(*Node->GetAttribute(TEXT("id")));
ReadTags(Node, Rel.Tags);
for (const FXmlNode* Child : Node->GetChildrenNodes())
{
if (Child->GetTag() == TEXT("member") && Child->GetAttribute(TEXT("type")) == TEXT("way"))
{
FOsmMember& Member = Rel.Members.AddDefaulted_GetRef();
Member.Role = Child->GetAttribute(TEXT("role"));
ReadPoints(Child, Member.Points);
}
}
}
}
return true;
}
/** Centroide par moyenne des sommets distincts (le point de fermeture repete n'est pas compte), comme l'importeur. */
template<typename VecType>
VecType Centroid(const TArray<VecType>& Points)
{
int32 Count = Points.Num();
if (Count > 1 && Points[0].Equals(Points[Count - 1], 1e-9))
{
--Count;
}
VecType Sum(0, 0);
for (int32 Index = 0; Index < Count; ++Index)
{
Sum += Points[Index];
}
return Count > 0 ? Sum / double(Count) : Sum;
}
/** Projection du plugin (Sanson-Flamsteed), relative a l'origine, en cm. */
FVector2d Project(const FLonLat& P, const FLonLat& Origin)
{
return FVector2d(
(P.X - Origin.X) * FMath::Cos(FMath::DegreesToRadians(P.Y)) * CmPerDegree,
-(P.Y - Origin.Y) * CmPerDegree);
}
double Median(TArray<double> Values)
{
if (Values.IsEmpty())
{
return 0.0;
}
Values.Sort();
const int32 Mid = Values.Num() / 2;
return (Values.Num() % 2) ? Values[Mid] : 0.5 * (Values[Mid - 1] + Values[Mid]);
}
/** Chaine des segments ouverts en anneaux fermes (extremites communes). Anneaux non fermables : comptes dans Broken. */
void ChainRings(TArray<TArray<FLonLat>> Segments, TArray<TArray<FLonLat>>& Rings, int32& Broken)
{
constexpr double Tol = 1e-7;
while (Segments.Num() > 0)
{
TArray<FLonLat> Ring = Segments.Pop();
bool bGrew = true;
while (!Ring[0].Equals(Ring.Last(), Tol) && bGrew)
{
bGrew = false;
for (int32 Index = 0; Index < Segments.Num(); ++Index)
{
TArray<FLonLat>& Seg = Segments[Index];
if (Seg[0].Equals(Ring.Last(), Tol))
{
Ring.Append(&Seg[1], Seg.Num() - 1);
}
else if (Seg.Last().Equals(Ring.Last(), Tol))
{
for (int32 P = Seg.Num() - 2; P >= 0; --P)
{
Ring.Add(Seg[P]);
}
}
else
{
continue;
}
Segments.RemoveAtSwap(Index);
bGrew = true;
break;
}
}
if (Ring.Num() >= 4 && Ring[0].Equals(Ring.Last(), Tol))
{
Rings.Add(MoveTemp(Ring));
}
else
{
++Broken;
}
}
}
/** Sutherland-Hodgman contre un rectangle (repere de la carte, cm). */
TArray<FVector2d> ClipToBox(const TArray<FVector2d>& In, const FVector2d& Min, const FVector2d& Max)
{
TArray<FVector2d> Out = In;
auto ClipEdge = [&Out](TFunctionRef<bool(const FVector2d&)> Inside, TFunctionRef<FVector2d(const FVector2d&, const FVector2d&)> Cut)
{
TArray<FVector2d> Src = MoveTemp(Out);
Out.Reset();
for (int32 Index = 0; Index < Src.Num(); ++Index)
{
const FVector2d& A = Src[Index];
const FVector2d& B = Src[(Index + 1) % Src.Num()];
const bool bA = Inside(A);
const bool bB = Inside(B);
if (bA) { Out.Add(A); }
if (bA != bB) { Out.Add(Cut(A, B)); }
}
};
auto LerpX = [](const FVector2d& A, const FVector2d& B, double X) { const double T = (X - A.X) / (B.X - A.X); return FVector2d(X, A.Y + T * (B.Y - A.Y)); };
auto LerpY = [](const FVector2d& A, const FVector2d& B, double Y) { const double T = (Y - A.Y) / (B.Y - A.Y); return FVector2d(A.X + T * (B.X - A.X), Y); };
ClipEdge([&](const FVector2d& P) { return P.X >= Min.X; }, [&](const FVector2d& A, const FVector2d& B) { return LerpX(A, B, Min.X); });
if (Out.Num() >= 3) ClipEdge([&](const FVector2d& P) { return P.X <= Max.X; }, [&](const FVector2d& A, const FVector2d& B) { return LerpX(A, B, Max.X); });
if (Out.Num() >= 3) ClipEdge([&](const FVector2d& P) { return P.Y >= Min.Y; }, [&](const FVector2d& A, const FVector2d& B) { return LerpY(A, B, Min.Y); });
if (Out.Num() >= 3) ClipEdge([&](const FVector2d& P) { return P.Y <= Max.Y; }, [&](const FVector2d& A, const FVector2d& B) { return LerpY(A, B, Max.Y); });
return Out.Num() >= 3 ? Out : TArray<FVector2d>();
}
}
ULPBuildCityWaterCommandlet::ULPBuildCityWaterCommandlet()
{
IsClient = false;
IsServer = false;
IsEditor = true;
LogToConsole = true;
}
int32 ULPBuildCityWaterCommandlet::Main(const FString& Params)
{
using namespace LPCityWater;
TArray<FString> Tokens;
TArray<FString> Switches;
TMap<FString, FString> ParamsMap;
ParseCommandLine(*Params, Tokens, Switches, ParamsMap);
const FString City = ParamsMap.FindRef(TEXT("City"));
const FString WaterPath = ParamsMap.FindRef(TEXT("Water"));
const FString BuildingsPath = ParamsMap.FindRef(TEXT("Buildings"));
const FString ReportPath = ParamsMap.FindRef(TEXT("Report"));
const bool bDryRun = Switches.Contains(TEXT("DryRun"));
const int32 MinPairs = ParamsMap.Contains(TEXT("MinPairs")) ? FCString::Atoi(*ParamsMap[TEXT("MinPairs")]) : 500;
const double MaxMedianCm = ParamsMap.Contains(TEXT("MaxMedianCm")) ? FCString::Atod(*ParamsMap[TEXT("MaxMedianCm")]) : 200.0;
TArray<FString> Report;
auto Line = [&Report](const FString& Text)
{
UE_LOG(LogLPBuildCityWater, 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;
};
if (City.IsEmpty() || WaterPath.IsEmpty() || BuildingsPath.IsEmpty())
{
Line(TEXT("usage : -City=<Ville> -Water=<q1.osm.xml> -Buildings=<q2.osm.xml> [-DryRun] [-Report=<f.md>] [-ListBuildingsInWater=<f.md>] [-ThresholdM2=<m2>] [-MaskOnly]"));
return Finish(2);
}
Line(FString::Printf(TEXT("# LPBuildCityWater %s (%s)"), *City, bDryRun ? TEXT("DryRun") : TEXT("ecriture")));
// 1. Carte et maillage de reference.
const UStreetMap* StreetMap = LoadObject<UStreetMap>(nullptr, *FString::Printf(TEXT("/Game/Heavy/StreetMap/%s.%s"), *City, *City));
const UStaticMesh* CityMesh = LoadObject<UStaticMesh>(nullptr, *FString::Printf(TEXT("/Game/Heavy/StreetMap/Static/%s.%s"), *City, *City));
if (!StreetMap || !CityMesh)
{
Line(TEXT("ECHEC : UStreetMap ou maillage Static introuvable"));
return Finish(1);
}
const FVector2d MapMin = StreetMap->GetBoundsMin();
const FVector2d MapMax = StreetMap->GetBoundsMax();
const FBox MeshBox = CityMesh->GetBoundingBox();
Line(FString::Printf(TEXT("bornes UStreetMap (cm) : (%.0f, %.0f) a (%.0f, %.0f)"), MapMin.X, MapMin.Y, MapMax.X, MapMax.Y));
Line(FString::Printf(TEXT("bornes maillage (cm) : (%.0f, %.0f) a (%.0f, %.0f)"), MeshBox.Min.X, MeshBox.Min.Y, MeshBox.Max.X, MeshBox.Max.Y));
const double BoundsGap = FMath::Max(
FMath::Max(FMath::Abs(MeshBox.Min.X - MapMin.X), FMath::Abs(MeshBox.Min.Y - MapMin.Y)),
FMath::Max(FMath::Abs(MeshBox.Max.X - MapMax.X), FMath::Abs(MeshBox.Max.Y - MapMax.Y)));
Line(FString::Printf(TEXT("ecart des bornes maillage / UStreetMap : %.0f cm (critere <= 100 ; indicatif : les bornes UStreetMap ne couvrent que les objets importes)"), BoundsGap));
// 2. Extraits OSM.
TArray<FOsmWay> WaterWays, BuildingWays;
TArray<FOsmRelation> WaterRelations, UnusedRelations;
FString Error;
if (!ReadOverpass(WaterPath, WaterWays, WaterRelations, Error) || !ReadOverpass(BuildingsPath, BuildingWays, UnusedRelations, Error))
{
Line(FString::Printf(TEXT("ECHEC : %s"), *Error));
return Finish(1);
}
Line(FString::Printf(TEXT("extraits : eau %d voies, %d relations ; batiments %d voies"), WaterWays.Num(), WaterRelations.Num(), BuildingWays.Num()));
// 3. Recalage : batiments nommes, noms uniques des deux cotes.
// STM1-B2 (R1) : l'extrait Overpass des batiments ne donne que <bounds> (pas de noeuds). On compare donc des centres de
// boite des deux cotes (meme definition) : moins precis qu'un centroide, biais possible sur les grands batiments
// irreguliers ; la mediane et la MAD des residus le mesurent.
auto BoxCenter = [](const TArray<FVector2d>& Points)
{
FVector2d Min(TNumericLimits<double>::Max(), TNumericLimits<double>::Max());
FVector2d Max(TNumericLimits<double>::Lowest(), TNumericLimits<double>::Lowest());
for (const FVector2d& P : Points)
{
Min = FVector2d(FMath::Min(Min.X, P.X), FMath::Min(Min.Y, P.Y));
Max = FVector2d(FMath::Max(Max.X, P.X), FMath::Max(Max.Y, P.Y));
}
return 0.5 * (Min + Max);
};
TMap<FString, int32> AssetCount, OsmCount;
TMap<FString, FVector2d> AssetCentroid;
TMap<FString, FLonLat> OsmCentroid;
for (const FStreetMapBuilding& Building : StreetMap->GetBuildings())
{
if (Building.BuildingName.IsEmpty() || Building.BuildingPoints.Num() < 3) continue;
const FString Key = Building.BuildingName.ToLower();
AssetCount.FindOrAdd(Key)++;
AssetCentroid.Add(Key, BoxCenter(Building.BuildingPoints));
}
int32 OsmFromBounds = 0, OsmFromNodes = 0;
for (const FOsmWay& Way : BuildingWays)
{
const FString* Name = Way.Tags.Find(TEXT("name"));
if (!Name || (!Way.bHasBounds && Way.Points.Num() < 3)) continue;
const FString Key = Name->ToLower();
OsmCount.FindOrAdd(Key)++;
if (Way.Points.Num() >= 3)
{
OsmCentroid.Add(Key, BoxCenter(Way.Points));
++OsmFromNodes;
}
else
{
OsmCentroid.Add(Key, Way.BoundsCenter);
++OsmFromBounds;
}
}
Line(FString::Printf(TEXT("batiments nommes : asset %d noms, OSM %d noms (centre de <bounds> : %d, centre de boite des noeuds : %d) ; methode : centres de boite des deux cotes"),
AssetCount.Num(), OsmCount.Num(), OsmFromBounds, OsmFromNodes));
TArray<TPair<FVector2d, FLonLat>> Pairs;
for (const TPair<FString, int32>& Entry : AssetCount)
{
if (Entry.Value == 1 && OsmCount.FindRef(Entry.Key) == 1)
{
Pairs.Add({ AssetCentroid[Entry.Key], OsmCentroid[Entry.Key] });
}
}
// STM1-B2 (S1, decision de Big Boss) : deux passes. Passe 1 : origine = mediane sur toutes les paires. Passe 2 : rejet des
// paires au-dela de mediane + 3 x MAD, origine recalculee sur les paires retenues. Criteres d'acceptation non circulaires :
// au moins MinPairs paires retenues, residu median (paires retenues) <= MaxMedianCm, taux de paires retenues >= 70 %.
// Pour information : part sous 5 m sur TOUTES les paires avec l'origine corrigee, et deplacement de l'origine (cm).
auto EstimateOrigin = [](const TArray<TPair<FVector2d, FLonLat>>& InPairs)
{
TArray<double> RelLats, RelLons;
for (const TPair<FVector2d, FLonLat>& Pair : InPairs)
{
const FVector2d& XY = Pair.Key;
const FLonLat& LL = Pair.Value;
RelLats.Add(LL.Y + XY.Y / CmPerDegree);
RelLons.Add(LL.X - XY.X / (CmPerDegree * FMath::Cos(FMath::DegreesToRadians(LL.Y))));
}
return FLonLat(Median(RelLons), Median(RelLats));
};
auto ResidualsOf = [](const TArray<TPair<FVector2d, FLonLat>>& InPairs, const FLonLat& InOrigin)
{
TArray<double> Out;
for (const TPair<FVector2d, FLonLat>& Pair : InPairs)
{
Out.Add(FVector2d::Distance(Project(Pair.Value, InOrigin), Pair.Key));
}
return Out;
};
auto MadOf = [](const TArray<double>& Values, double Center)
{
TArray<double> Deviations;
for (const double V : Values)
{
Deviations.Add(FMath::Abs(V - Center));
}
return Median(Deviations);
};
auto ShareUnder = [](const TArray<double>& Values, double Limit)
{
int32 Count = 0;
for (const double V : Values) { Count += V <= Limit ? 1 : 0; }
return Values.Num() ? double(Count) / Values.Num() : 0.0;
};
// Passe 1.
const FLonLat Origin1 = EstimateOrigin(Pairs);
const TArray<double> Residuals1 = ResidualsOf(Pairs, Origin1);
const double Median1 = Median(Residuals1);
const double Mad1 = MadOf(Residuals1, Median1);
Line(FString::Printf(TEXT("passe 1 : %d paires ; origine lat %.7f lon %.7f ; residu median %.0f cm ; MAD %.0f cm ; %.1f %% sous 5 m"),
Pairs.Num(), Origin1.Y, Origin1.X, Median1, Mad1, 100.0 * ShareUnder(Residuals1, 500.0)));
// Passe 2 : rejet a mediane + 3 x MAD.
const double RejectCm = Median1 + 3.0 * Mad1;
TArray<TPair<FVector2d, FLonLat>> Kept;
for (int32 Index = 0; Index < Pairs.Num(); ++Index)
{
if (Residuals1[Index] <= RejectCm)
{
Kept.Add(Pairs[Index]);
}
}
const FLonLat Origin = Kept.Num() ? EstimateOrigin(Kept) : Origin1;
const TArray<double> ResidualsKept = ResidualsOf(Kept, Origin);
const double MedianKept = Median(ResidualsKept);
const double KeptRate = Pairs.Num() ? double(Kept.Num()) / Pairs.Num() : 0.0;
const double ShareAllUnder5m = ShareUnder(ResidualsOf(Pairs, Origin), 500.0);
const double OriginShiftCm = Project(Origin1, Origin).Length();
// Preuve que la passe 2 recalcule l'origine (demande de Big Boss) : valeurs en pleine precision et decalage en mm.
Line(FString::Printf(TEXT("origines pleine precision : passe 1 lat %.12f lon %.12f ; passe 2 lat %.12f lon %.12f ; ecart lat %.3e deg, lon %.3e deg ; decalage %.3f mm ; passe 2 calculee sur %d paires (passe 1 : %d)"),
Origin1.Y, Origin1.X, Origin.Y, Origin.X, Origin.Y - Origin1.Y, Origin.X - Origin1.X, OriginShiftCm * 10.0, Kept.Num(), Pairs.Num()));
Line(FString::Printf(TEXT("passe 2 : seuil de rejet %.0f cm (mediane + 3 x MAD) ; %d paires retenues, %d ecartees (taux retenu %.1f %%) ; origine lat %.7f lon %.7f ; residu median (retenues) %.0f cm"),
RejectCm, Kept.Num(), Pairs.Num() - Kept.Num(), 100.0 * KeptRate, Origin.Y, Origin.X, MedianKept));
Line(FString::Printf(TEXT("information : %.1f %% sous 5 m sur toutes les paires avec l'origine corrigee ; deplacement de l'origine passe 1 -> passe 2 : %.0f cm"),
100.0 * ShareAllUnder5m, OriginShiftCm));
Line(TEXT("note : centres de boite moins precis qu'un centroide (biais possible sur les grands batiments) ; le juge final est l'essai visuel (eau sur les quais)"));
if (Kept.Num() < MinPairs || MedianKept > MaxMedianCm || KeptRate < 0.70)
{
Line(FString::Printf(TEXT("ECHEC : criteres de recalage non atteints (paires retenues %d >= %d ; residu median %.0f <= %.0f cm ; taux retenu %.1f >= 70 %%) ; aucun asset ecrit"),
Kept.Num(), MinPairs, MedianKept, MaxMedianCm, 100.0 * KeptRate));
return Finish(1);
}
Line(TEXT("recalage accepte (criteres : paires retenues, residu median, taux retenu)"));
// 4. Polygones d'eau (anneaux en lon/lat), puis projection et decoupe a la boite de la carte + 50 m.
TArray<TArray<FLonLat>> OuterLL, InnerLL;
int32 Broken = 0;
for (const FOsmWay& Way : WaterWays)
{
if (Way.Points.Num() >= 4 && Way.Points[0].Equals(Way.Points.Last(), 1e-7))
{
OuterLL.Add(Way.Points);
}
}
for (const FOsmRelation& Rel : WaterRelations)
{
TArray<TArray<FLonLat>> OuterSegs, InnerSegs;
for (const FOsmMember& Member : Rel.Members)
{
if (Member.Points.Num() < 2) continue;
(Member.Role == TEXT("inner") ? InnerSegs : OuterSegs).Add(Member.Points);
}
ChainRings(MoveTemp(OuterSegs), OuterLL, Broken);
ChainRings(MoveTemp(InnerSegs), InnerLL, Broken);
}
const FVector2d ClipMin = MapMin - FVector2d(5000.0, 5000.0);
const FVector2d ClipMax = MapMax + FVector2d(5000.0, 5000.0);
auto ToMap = [&](const TArray<FLonLat>& Ring)
{
TArray<FVector2d> P;
for (int32 Index = 0; Index + 1 < Ring.Num(); ++Index) // sans le point de fermeture
{
P.Add(Project(Ring[Index], Origin));
}
return ClipToBox(P, ClipMin, ClipMax);
};
TArray<TPolygon2<double>> Outers, Inners;
for (const TArray<FLonLat>& Ring : OuterLL) { TArray<FVector2d> P = ToMap(Ring); if (P.Num() >= 3) Outers.Emplace(P); }
for (const TArray<FLonLat>& Ring : InnerLL) { TArray<FVector2d> P = ToMap(Ring); if (P.Num() >= 3) Inners.Emplace(P); }
Line(FString::Printf(TEXT("polygones dans la carte : %d exterieurs, %d trous ; anneaux non fermes ignores : %d"), Outers.Num(), Inners.Num(), Broken));
// 5. Triangulation (exterieur anti-horaire, trous horaires, trou rattache a l'exterieur qui contient son premier sommet).
TArray<FVector3f> Vertices;
TArray<FIntVector3> Triangles;
double AreaCm2 = 0.0;
TArray<TGeneralPolygon2<double>> WaterPolygons;
for (TPolygon2<double>& Outer : Outers)
{
if (Outer.IsClockwise()) Outer.Reverse();
TGeneralPolygon2<double> General(Outer);
for (TPolygon2<double> Hole : Inners)
{
if (Outer.Contains(Hole.GetVertices()[0]))
{
if (!Hole.IsClockwise()) Hole.Reverse();
General.AddHole(Hole, false, false);
}
}
const TArray<FIndex3i> Tris = ConstrainedDelaunayTriangulate<double>(General);
const int32 Base = Vertices.Num();
for (const FVector2d& V : General.GetOuter().GetVertices()) Vertices.Emplace(float(V.X), float(V.Y), 0.f);
for (const TPolygon2<double>& H : General.GetHoles()) for (const FVector2d& V : H.GetVertices()) Vertices.Emplace(float(V.X), float(V.Y), 0.f);
for (const FIndex3i& T : Tris) Triangles.Emplace(Base + T.A, Base + T.B, Base + T.C);
AreaCm2 += FMath::Abs(General.SignedArea());
WaterPolygons.Add(MoveTemp(General));
}
Line(FString::Printf(TEXT("maillage : %d sommets, %d triangles, aire %.3f km2"), Vertices.Num(), Triangles.Num(), AreaCm2 / 1e10));
if (Triangles.IsEmpty())
{
Line(TEXT("ECHEC : aucun triangle"));
return Finish(1);
}
// STM1-SPEC-04 (O3) : batiments de la carte dont le centre tombe dans l'eau. Centre et aire comme MapMarkers
// (GI.FindCenterOfPolygon / calculateArea : boite englobante en float, aire = dX x dY / 1000) ; aire du polygone en m2 en plus.
const FString ListPath = ParamsMap.FindRef(TEXT("ListBuildingsInWater"));
const bool bMaskOnly = Switches.Contains(TEXT("MaskOnly"));
if (!ListPath.IsEmpty() || bMaskOnly)
{
const double ThresholdM2 = ParamsMap.Contains(TEXT("ThresholdM2")) ? FCString::Atod(*ParamsMap[TEXT("ThresholdM2")]) : 1500.0;
struct FInWater { FString Name; FString Amenity; double PolyM2; double BoxM2; double GameArea; FVector2d Center; int32 Index; };
TArray<FInWater> InWater;
const TArray<FStreetMapBuilding>& Buildings = StreetMap->GetBuildings();
for (int32 Index = 0; Index < Buildings.Num(); ++Index)
{
const FStreetMapBuilding& Building = Buildings[Index];
if (Building.BuildingPoints.Num() < 3) continue;
float MinX = float(Building.BuildingPoints[0].X), MaxX = MinX, MinY = float(Building.BuildingPoints[0].Y), MaxY = MinY;
double Shoelace = 0.0;
for (int32 P = 0; P < Building.BuildingPoints.Num(); ++P)
{
const FVector2D& A = Building.BuildingPoints[P];
const FVector2D& B = Building.BuildingPoints[(P + 1) % Building.BuildingPoints.Num()];
MinX = FMath::Min(MinX, float(A.X)); MaxX = FMath::Max(MaxX, float(A.X));
MinY = FMath::Min(MinY, float(A.Y)); MaxY = FMath::Max(MaxY, float(A.Y));
Shoelace += A.X * B.Y - B.X * A.Y;
}
const FVector2d Center((double(MinX) + double(MaxX)) / 2.0, (double(MinY) + double(MaxY)) / 2.0);
bool bInside = false;
for (const TGeneralPolygon2<double>& Water : WaterPolygons)
{
if (Water.Bounds().Contains(Center) && Water.Contains(Center)) { bInside = true; break; }
}
if (!bInside) continue;
const double BoxCm2 = (double(MaxX) - double(MinX)) * (double(MaxY) - double(MinY));
InWater.Add({ Building.BuildingName, Building.Amenity, FMath::Abs(Shoelace) / 2.0 / 1e4, BoxCm2 / 1e4, BoxCm2 / 1000.0, Center, Index });
}
InWater.Sort([](const FInWater& A, const FInWater& B) { return A.PolyM2 > B.PolyM2; });
int32 Above = 0, Named = 0;
for (const FInWater& B : InWater) { Above += B.PolyM2 >= ThresholdM2 ? 1 : 0; Named += B.Name.IsEmpty() ? 0 : 1; }
TArray<FString> List;
List.Add(FString::Printf(TEXT("# Batiments de %s dont le centre tombe dans l'eau (seuil %.0f m2)"), *City, ThresholdM2));
List.Add(TEXT(""));
List.Add(FString::Printf(TEXT("total %d sur %d batiments ; nommes %d ; au-dessus du seuil (aire du polygone) %d ; en dessous %d"),
InWater.Num(), Buildings.Num(), Named, Above, InWater.Num() - Above));
List.Add(TEXT(""));
List.Add(TEXT("| # | indice | nom | amenite | aire polygone (m2) | aire boite (m2) | aire du jeu (dX*dY/1000) | echelle XY du jeu | centre X (cm) | centre Y (cm) | seuil |"));
List.Add(TEXT("|---|---|---|---|---|---|---|---|---|---|---|"));
for (int32 Row = 0; Row < InWater.Num(); ++Row)
{
const FInWater& B = InWater[Row];
const double Scale = FMath::GetMappedRangeValueClamped(FVector2d(0.0, 100000.0), FVector2d(2.0, 20.0), B.GameArea);
List.Add(FString::Printf(TEXT("| %d | %d | %s | %s | %.0f | %.0f | %.0f | %.2f | %.0f | %.0f | %s |"), Row + 1, B.Index,
B.Name.IsEmpty() ? TEXT("-") : *B.Name.Replace(TEXT("|"), TEXT("/")), B.Amenity.IsEmpty() ? TEXT("-") : *B.Amenity,
B.PolyM2, B.BoxM2, B.GameArea, Scale, B.Center.X, B.Center.Y, B.PolyM2 >= ThresholdM2 ? TEXT("epargne") : TEXT("masque")));
}
if (!ListPath.IsEmpty())
{
FFileHelper::SaveStringArrayToFile(List, *ListPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
}
Line(FString::Printf(TEXT("O3 : %d batiments dans l'eau (au-dessus de %.0f m2 : %d) ; liste %s"), InWater.Num(), ThresholdM2, Above,
ListPath.IsEmpty() ? TEXT("(non ecrite)") : *ListPath));
// STM1-SPEC-08 (O2) : masque /Game/Heavy/StreetMap/Water/<Ville>_Mask, sans relire ni reecrire le maillage d'eau.
if (bMaskOnly)
{
const FString MaskPackageName = FString::Printf(TEXT("/Game/Heavy/StreetMap/Water/%s_Mask"), *City);
if (bDryRun)
{
Line(FString::Printf(TEXT("DryRun : masque %s non ecrit (%d centres)"), *MaskPackageName, InWater.Num()));
return Finish(0);
}
if (FPackageName::DoesPackageExist(MaskPackageName))
{
Line(FString::Printf(TEXT("ECHEC : %s existe deja ; aucun asset ecrit"), *MaskPackageName));
return Finish(1);
}
UPackage* MaskPackage = CreatePackage(*MaskPackageName);
ULPCityWaterMask* Mask = NewObject<ULPCityWaterMask>(MaskPackage, *FPackageName::GetLongPackageAssetName(MaskPackageName), RF_Public | RF_Standalone);
Mask->City = City;
Mask->SourceBuildingCount = Buildings.Num();
for (const FInWater& B : InWater)
{
Mask->Centers.Add(B.Center);
Mask->AreasM2.Add(float(B.PolyM2));
}
FAssetRegistryModule::AssetCreated(Mask);
FSavePackageArgs MaskSaveArgs;
MaskSaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
const FString MaskFile = FPackageName::LongPackageNameToFilename(MaskPackageName, FPackageName::GetAssetPackageExtension());
if (!UPackage::SavePackage(MaskPackage, Mask, *MaskFile, MaskSaveArgs))
{
Line(FString::Printf(TEXT("ECHEC : sauvegarde de %s"), *MaskFile));
return Finish(1);
}
Line(FString::Printf(TEXT("asset ecrit : %s (%s), %d centres sur %d batiments ; maillage d'eau non touche"),
*MaskPackageName, *MaskFile, InWater.Num(), Buildings.Num()));
return Finish(0);
}
}
if (bDryRun)
{
Line(TEXT("DryRun : aucun asset ecrit"));
return Finish(0);
}
// 6. Asset /Game/Heavy/StreetMap/Water/<Ville> (plat, Z = 0, une section, M_LP_Water, sans collision).
// Garde : ne jamais ecraser un maillage d'eau existant.
const FString PackageName = FString::Printf(TEXT("/Game/Heavy/StreetMap/Water/%s"), *City);
if (FPackageName::DoesPackageExist(PackageName))
{
Line(FString::Printf(TEXT("ECHEC : %s existe deja ; aucun asset ecrit"), *PackageName));
return Finish(1);
}
const TCHAR* MaterialPackageName = TEXT("/Game/Heavy/StreetMap/Water/M_LP_Water");
UMaterialInterface* WaterMaterial = LoadObject<UMaterialInterface>(nullptr, TEXT("/Game/Heavy/StreetMap/Water/M_LP_Water.M_LP_Water"));
if (!WaterMaterial && Switches.Contains(TEXT("CreateMaterial")))
{
// STM1-SPEC-03 § 2.5 : opaque, deux faces, DefaultLit, couleur (0,03 ; 0,18 ; 0,32), rugosite 0,15, speculaire 0,5.
UPackage* MaterialPackage = CreatePackage(MaterialPackageName);
UMaterial* NewMaterial = NewObject<UMaterial>(MaterialPackage, TEXT("M_LP_Water"), RF_Public | RF_Standalone);
NewMaterial->TwoSided = true;
UMaterialExpressionConstant3Vector* Color = Cast<UMaterialExpressionConstant3Vector>(
UMaterialEditingLibrary::CreateMaterialExpression(NewMaterial, UMaterialExpressionConstant3Vector::StaticClass(), -300, 0));
UMaterialExpressionConstant* Roughness = Cast<UMaterialExpressionConstant>(
UMaterialEditingLibrary::CreateMaterialExpression(NewMaterial, UMaterialExpressionConstant::StaticClass(), -300, 150));
UMaterialExpressionConstant* Specular = Cast<UMaterialExpressionConstant>(
UMaterialEditingLibrary::CreateMaterialExpression(NewMaterial, UMaterialExpressionConstant::StaticClass(), -300, 250));
if (!Color || !Roughness || !Specular)
{
Line(TEXT("ECHEC : creation des expressions du materiau ; aucun asset ecrit"));
return Finish(1);
}
Color->Constant = FLinearColor(0.03f, 0.18f, 0.32f);
Roughness->R = 0.15f;
Specular->R = 0.5f;
const bool bLinked = UMaterialEditingLibrary::ConnectMaterialProperty(Color, TEXT(""), MP_BaseColor)
&& UMaterialEditingLibrary::ConnectMaterialProperty(Roughness, TEXT(""), MP_Roughness)
&& UMaterialEditingLibrary::ConnectMaterialProperty(Specular, TEXT(""), MP_Specular);
UMaterialEditingLibrary::RecompileMaterial(NewMaterial);
FAssetRegistryModule::AssetCreated(NewMaterial);
FSavePackageArgs MaterialSaveArgs;
MaterialSaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
const FString MaterialFile = FPackageName::LongPackageNameToFilename(MaterialPackageName, FPackageName::GetAssetPackageExtension());
if (!bLinked || !UPackage::SavePackage(MaterialPackage, NewMaterial, *MaterialFile, MaterialSaveArgs))
{
Line(FString::Printf(TEXT("ECHEC : materiau (liaisons %d) ou sauvegarde de %s"), bLinked ? 1 : 0, *MaterialFile));
return Finish(1);
}
Line(FString::Printf(TEXT("materiau cree : %s (%s), deux faces"), MaterialPackageName, *MaterialFile));
WaterMaterial = NewMaterial;
}
if (!WaterMaterial)
{
Line(TEXT("ECHEC : materiau /Game/Heavy/StreetMap/Water/M_LP_Water absent (option -CreateMaterial, STM1-SPEC-03 § 2.5)"));
return Finish(1);
}
FMeshDescription Mesh;
FStaticMeshAttributes Attributes(Mesh);
Attributes.Register();
const FPolygonGroupID Group = Mesh.CreatePolygonGroup();
Attributes.GetPolygonGroupMaterialSlotNames()[Group] = TEXT("Water");
TArray<FVertexID> VertexIds;
VertexIds.Reserve(Vertices.Num());
for (const FVector3f& V : Vertices)
{
const FVertexID Id = Mesh.CreateVertex();
Attributes.GetVertexPositions()[Id] = V;
VertexIds.Add(Id);
}
for (const FIntVector3& T : Triangles)
{
TArray<FVertexInstanceID> Corners;
for (const int32 Index : { T.X, T.Y, T.Z })
{
const FVertexInstanceID Instance = Mesh.CreateVertexInstance(VertexIds[Index]);
Attributes.GetVertexInstanceNormals()[Instance] = FVector3f::UpVector;
Attributes.GetVertexInstanceUVs().Set(Instance, 0, FVector2f(Vertices[Index].X / 100.f, Vertices[Index].Y / 100.f));
Corners.Add(Instance);
}
Mesh.CreatePolygon(Group, Corners);
}
UPackage* Package = CreatePackage(*PackageName);
UStaticMesh* WaterMesh = NewObject<UStaticMesh>(Package, *City, RF_Public | RF_Standalone);
WaterMesh->GetStaticMaterials().Add(FStaticMaterial(WaterMaterial, TEXT("Water")));
UStaticMesh::FBuildMeshDescriptionsParams BuildParams;
BuildParams.bBuildSimpleCollision = false;
BuildParams.bCommitMeshDescription = true;
BuildParams.bMarkPackageDirty = true;
WaterMesh->BuildFromMeshDescriptions({ &Mesh }, BuildParams);
FAssetRegistryModule::AssetCreated(WaterMesh);
FSavePackageArgs SaveArgs;
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
const FString FileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());
if (!UPackage::SavePackage(Package, WaterMesh, *FileName, SaveArgs))
{
Line(FString::Printf(TEXT("ECHEC : sauvegarde de %s"), *FileName));
return Finish(1);
}
Line(FString::Printf(TEXT("asset ecrit : %s (%s)"), *PackageName, *FileName));
return Finish(0);
}