From df40ca92e56dd94f794d1c11f3027cac9e32aa16 Mon Sep 17 00:00:00 2001 From: Steve Streeting Date: Thu, 11 Jan 2024 15:32:59 +0000 Subject: [PATCH] Add Fill2DRegionWithRectangles function --- .../Private/StevesMathHelpers.cpp | 125 ++++++++++++++++++ .../Public/StevesMathHelpers.h | 29 ++++ 2 files changed, 154 insertions(+) diff --git a/Source/StevesUEHelpers/Private/StevesMathHelpers.cpp b/Source/StevesUEHelpers/Private/StevesMathHelpers.cpp index 6331e54..55f9ea9 100644 --- a/Source/StevesUEHelpers/Private/StevesMathHelpers.cpp +++ b/Source/StevesUEHelpers/Private/StevesMathHelpers.cpp @@ -106,3 +106,128 @@ float StevesMathHelpers::GetDistanceToConvex2D(const TArray& ConvexPo return bInside ? -ClosestInside : ClosestOutside; } + +int StevesMathHelpers::Fill2DRegionWithRectangles(int StartX, + int StartY, + int Width, + int Height, + std::function CellIncludeFunc, + TArray& OutRects) +{ + int RectCount = 0; + + const int Len = Width*Height; + TArray bDoneMarkers; + bDoneMarkers.SetNumUninitialized(Len); + int CellsTodo = 0; + + int StartN = 0; + const int EndX = StartX + Width - 1; + const int EndY = StartY + Height - 1; + // Initialise done markers based on func + for (int y = 0; y < Height; ++y) + { + for (int x = 0; x < Width; ++x) + { + const bool Include = CellIncludeFunc(x+StartX, y+StartY); + bDoneMarkers[y*Width + x] = !Include; + if (Include) + { + if (++CellsTodo == 1) + { + // This is the one we'll start with, might as well calculate it while we're here + StartN = y*Width + x; + } + } + } + } + + while (CellsTodo > 0) + { + // Find next starting point, from last one, until not done + for (; StartN < Len && bDoneMarkers[StartN]; ++StartN) {} + + // Shouldn't happen, but just in case + if (StartN >= Len) + break; + + // NOTE: this X/Y is local (based at 0,0 not StartX/StartY, for use with DoneMarkers) + const int LocalStartX = StartN % Width; + const int LocalStartY = StartN / Width; + + // We try not to create long & thin rects if we can help it, unlike greedy meshing + // We're greedy in alternate dims to try to make fatter quads + bool bCanExtendX = true, bCanExtendY = true; + bool bExtendingX = true; + int W = 1; + int H = 1; + + while (bCanExtendX || bCanExtendY) + { + // Try extending right + if (bExtendingX) + { + bool ExtendOK = LocalStartX+W < Width; + for (int TestY = LocalStartY; ExtendOK && TestY < LocalStartY+H; ++TestY) + { + if (bDoneMarkers[TestY*Width + LocalStartX+W]) + { + // No good + ExtendOK = false; + } + } + if (ExtendOK) + { + ++W; + } + else + { + bCanExtendX = false; + } + // Flip extending axis if possible + if (bCanExtendY) + bExtendingX = false; + } + else + { + // Try extending down + bool ExtendOK = LocalStartY+H < Height; + for (int TestX = LocalStartX; ExtendOK && TestX < LocalStartX+W; ++TestX) + { + if (bDoneMarkers[(LocalStartY+H)*Width + TestX]) + { + // No good + ExtendOK = false; + } + } + if (ExtendOK) + { + ++H; + } + else + { + bCanExtendY = false; + } + // Flip extending axis if possible + if (bCanExtendX) + bExtendingX = true; + } + } + + // We've calculated the max extension + for (int y = LocalStartY; y < LocalStartY+H; ++y) + { + for (int x = LocalStartX; x < LocalStartX+W; ++x) + { + bDoneMarkers[y*Width+x] = true; + --CellsTodo; + } + } + OutRects.Add(FIntRect(StartX+LocalStartX, StartY+LocalStartY, StartX+LocalStartX+W-1, StartY+LocalStartY+H-1)); + ++RectCount; + + } + + return RectCount; + +} diff --git a/Source/StevesUEHelpers/Public/StevesMathHelpers.h b/Source/StevesUEHelpers/Public/StevesMathHelpers.h index 62c3628..2d16217 100644 --- a/Source/StevesUEHelpers/Public/StevesMathHelpers.h +++ b/Source/StevesUEHelpers/Public/StevesMathHelpers.h @@ -1,4 +1,5 @@ #pragma once +#include struct FKConvexElem; @@ -147,4 +148,32 @@ public: const float d = (v2.X - v1.X) * (p.Y - v1.Y) - (v2.Y - v1.Y) * (p.X - v1.X); return d == 0 || (d < 0) == (s + t <= 0); } + + /** + * Function that tries to fill a 2D area with the largest rectangles it can. The area is abstractly defined as a boundary + * index area with start X/Y and width/height, and will call back the CellIncludeFunc to determine whether a given + * cell index X/Y should be considered valid to include in a rectangle. This means you can define irregular grids of + * "valid" cells, and this function will fill the area with the largest rectangles it can while staying out of "invalid" + * cells. + * If you return "true" from every call to your CellIncludeFunc then the result will be a single rectangle covering + * the entire area. It's expected that you will return "false" for some X/Y combinations and that will cause the area + * to be split into multiple rectangles. + * The returned rectangles will not overlap, and the entire valid area will be filled. + * @param StartX The start X index. This is defined by your own data, so you can address a subset if you want. + * @param StartY The start Y index.This is defined by your own data, so you can address a subset if you want. + * @param Width The width of the area to fill. This is defined by your own data, so you can address a subset if you want. + * @param Height The height of the area to fill. This is defined by your own data, so you can address a subset if you want. + * @param CellIncludeFunc Your function which given an X/Y cell index, must return true if that cell is valid to be + * included in a rectangle. + * @param OutRects Array of rectangles which this function should append results to. Will not be cleared before adding. + * @return The number of rectangles added by this call. Each rectangle is a min/max inclusive X/Y value. + */ + static int Fill2DRegionWithRectangles(int StartX, + int StartY, + int Width, + int Height, + std::function CellIncludeFunc, + TArray& OutRects); + + }; \ No newline at end of file