using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace Com.ForbiddenByte.OSA.Util
{
[AddComponentMenu("Layout/Packed Grid Layout Group", 153)]
///
/// Layout class to arrange children elements in a circular grid format. Circular in the . Items will try to occupy as much space as possible.
///
public class PackedGridLayoutGroup : LayoutGroup
{
[SerializeField] protected float m_ForcedSpacing = 0f;
[Tooltip("The specified axis will have the 'Preferred' size set based on children")]
[SerializeField] protected AxisOrNone m_childrenControlSize = AxisOrNone.Vertical;
[Tooltip("If true, the layout will start with bigger children. The starting position is defined by the 'Child Alignment' property")]
[SerializeField] protected bool m_biggerChildrenFirst = true;
[Tooltip("Set to as many as possible, if the FPS allows")]
[Range(1, (int)Packer2DBox.NodeChoosingStrategy.COUNT_)]
[SerializeField] protected int m_numPasses = (int)Packer2DBox.NodeChoosingStrategy.COUNT_;
///
/// The spacing to use between layout elements in the grid on both axes.
/// The spacing is created by shrinking the childrens' sizes rather than actually adding spaces.
/// If you want true spacing, consider modifying the children themselves to also include some padding inside them
///
public float ForcedSpacing { get { return m_ForcedSpacing; } set { SetProperty(ref m_ForcedSpacing, value); } }
///
/// The specified axis will have the 'Preferred' size set based on children
///
public AxisOrNone ChildrenControlSize { get { return m_childrenControlSize; } set { SetProperty(ref m_childrenControlSize, value); } }
///
/// If true, the layout will start with bigger children. The starting position is defined by the 'Child Alignment' property
///
public bool BiggerChildrenFirst { get { return m_biggerChildrenFirst; } set { SetProperty(ref m_biggerChildrenFirst, value); } }
///
/// This refers to the number of different strategies to use when packing children. Set to as many as possible, if the FPS allows.
/// See .
/// At the moment (15 Mar 2019), more than 1 pass is executing only if the boxes don't all fit in the available
/// space, as the first strategy () seems to always perform the best
///
public int NumPasses { get { return m_numPasses; } set { SetProperty(ref m_numPasses, value); } }
protected PackedGridLayoutGroup()
{ }
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
}
#endif
public override void CalculateLayoutInputHorizontal()
{
base.CalculateLayoutInputHorizontal();
float minWidthToSet;
float preferredWidthToSet;
if (m_childrenControlSize == AxisOrNone.Horizontal)
{
float width, _;
GetChildSetups(out width, out _);
minWidthToSet = preferredWidthToSet = width + padding.horizontal;
}
else
{
minWidthToSet = minWidth;
preferredWidthToSet = preferredWidth;
}
SetLayoutInputForAxis(minWidthToSet, preferredWidthToSet, -1, 0);
}
public override void CalculateLayoutInputVertical()
{
float minHeightToSet;
float preferredHeightToSet;
if (m_childrenControlSize == AxisOrNone.Vertical)
{
float _, height;
GetChildSetups(out _, out height);
minHeightToSet = preferredHeightToSet = height + padding.vertical;
}
else
{
minHeightToSet = minHeight;
preferredHeightToSet = preferredHeight;
}
SetLayoutInputForAxis(minHeightToSet, preferredHeightToSet, -1, 1);
}
public override void SetLayoutHorizontal()
{
LayoutChildren(true, false);
}
public override void SetLayoutVertical()
{
LayoutChildren(false, true);
}
void GetInsetAndEdges(float chWidth, float chHeight, out RectTransform.Edge xInsetEdge, out RectTransform.Edge yInsetEdge, out float xAddInset, out float yAddInset)
{
var gridRect = rectTransform.rect;
xInsetEdge = RectTransform.Edge.Left;
yInsetEdge = RectTransform.Edge.Top;
xAddInset = 0f;
yAddInset = 0f;
if (m_childrenControlSize != AxisOrNone.Horizontal)
{
switch (childAlignment)
{
case TextAnchor.UpperLeft:
case TextAnchor.LowerLeft:
case TextAnchor.MiddleLeft:
xInsetEdge = RectTransform.Edge.Left;
xAddInset = padding.left;
break;
case TextAnchor.UpperRight:
case TextAnchor.LowerRight:
case TextAnchor.MiddleRight:
xInsetEdge = RectTransform.Edge.Right;
xAddInset = padding.right;
break;
case TextAnchor.UpperCenter:
case TextAnchor.MiddleCenter:
case TextAnchor.LowerCenter:
xAddInset = Mathf.Max((gridRect.width - chWidth), padding.horizontal) / 2f;
break;
}
}
if (m_childrenControlSize != AxisOrNone.Vertical)
{
switch (childAlignment)
{
case TextAnchor.UpperLeft:
case TextAnchor.UpperCenter:
case TextAnchor.UpperRight:
yInsetEdge = RectTransform.Edge.Top;
yAddInset = padding.top;
break;
case TextAnchor.LowerLeft:
case TextAnchor.LowerCenter:
case TextAnchor.LowerRight:
yInsetEdge = RectTransform.Edge.Bottom;
yAddInset = padding.bottom;
break;
case TextAnchor.MiddleLeft:
case TextAnchor.MiddleCenter:
case TextAnchor.MiddleRight:
yAddInset = Mathf.Max((gridRect.height - chHeight), padding.vertical) / 2f;
break;
}
}
}
void LayoutChildren(bool hor, bool vert)
{
float chWidth, chHeight;
var setups = GetChildSetups(out chWidth, out chHeight);
RectTransform.Edge xInsetEdge, yInsetEdge;
float xAddInset, yAddInset;
GetInsetAndEdges(chWidth, chHeight, out xInsetEdge, out yInsetEdge, out xAddInset, out yAddInset);
foreach (var child in setups)
{
var c = child as ChildSetup;
if (c.box.position == null)
continue;
if (hor)
c.child.SetInsetAndSizeFromParentEdge(xInsetEdge, (float)c.box.position.x + xAddInset, (float)c.box.width - ForcedSpacing);
if (vert)
c.child.SetInsetAndSizeFromParentEdge(yInsetEdge, (float)c.box.position.y + yAddInset, (float)c.box.height - ForcedSpacing);
}
}
List GetChildSetups(out float width, out float height)
{
var list = new List(rectChildren.Count);
foreach (var child in rectChildren)
{
float chWidth = LayoutUtility.GetPreferredSize(child, 0);
float chHeight = LayoutUtility.GetPreferredSize(child, 1);
list.Add(new ChildSetup(chWidth, chHeight, child));
}
if (BiggerChildrenFirst)
{
// Biggest boxes first with maxside, then secondarily by volume
// More info: https://codeincomplete.com/posts/bin-packing/
list.Sort((a, b) =>
{
var aMax = System.Math.Max(a.box.width, a.box.height);
var bMax = System.Math.Max(b.box.width, b.box.height);
if (aMax != bMax)
return (int)(bMax - aMax);
return (int)(b.box.volume - a.box.volume);
});
}
float availableWidth, availableHeight;
if (m_childrenControlSize == AxisOrNone.Horizontal)
{
availableWidth = float.MaxValue;
availableHeight = rectTransform.rect.height - padding.vertical;
}
else if (m_childrenControlSize == AxisOrNone.Vertical)
{
availableWidth = rectTransform.rect.width - padding.horizontal;
availableHeight = float.MaxValue;
}
else
{
availableHeight = rectTransform.rect.height - padding.vertical;
availableWidth = rectTransform.rect.width - padding.horizontal;
}
// Spacing usually creates mode big empty spaces where no item can fit
float spacingToUse = 0f;
var packer = new Packer2DBox(availableWidth, availableHeight, spacingToUse);
int maxStrategiesToUse = m_numPasses;
List[] boxesPerStrategy = new List[maxStrategiesToUse];
int[] nullPositionsPerStrategy = new int[maxStrategiesToUse];
double[] totalWidthsPerStrategy = new double[maxStrategiesToUse];
double[] totalHeightsPerStrategy = new double[maxStrategiesToUse];
int iBest = -1;
bool copyBoxesForFinalResult = maxStrategiesToUse > 1;
for (int i = 0; i < maxStrategiesToUse; i++)
{
var boxesThisPass = i == 0 ? list.ConvertAll(c => c.box) : list.ConvertAll(c => { c.ReinitBox(); return c.box; });
double totalWidthThisPass;
double totalHeightThisPass;
packer.Pack(boxesThisPass, false, (Packer2DBox.NodeChoosingStrategy)i, out totalWidthThisPass, out totalHeightThisPass);
int thisPassNullPositions = 0;
for (int j = 0; j < boxesThisPass.Count; j++)
{
var b = boxesThisPass[j];
if (b.position == null)
++thisPassNullPositions;
}
boxesPerStrategy[i] = boxesThisPass;
nullPositionsPerStrategy[i] = thisPassNullPositions;
totalWidthsPerStrategy[i] = totalWidthThisPass;
totalHeightsPerStrategy[i] = totalHeightThisPass;
if (iBest == -1)
{
iBest = i;
if (thisPassNullPositions == 0)
{
// Boxes won't be overridden by next strategies, so no need to copy them
copyBoxesForFinalResult = false;
// First pass is Packer2DBox.NodeChoosingStrategy.MAX_VOLUME and all boxes were fit => there's no better strategy
break;
}
continue;
}
if (thisPassNullPositions < nullPositionsPerStrategy[iBest])
{
iBest = i;
continue;
}
if (thisPassNullPositions > nullPositionsPerStrategy[iBest])
continue;
if (totalWidthThisPass * totalHeightThisPass < totalWidthsPerStrategy[i] * totalHeightsPerStrategy[i])
{
iBest = i;
continue;
}
}
if (copyBoxesForFinalResult)
{
var bestBoxes = boxesPerStrategy[iBest];
for (int i = 0; i < list.Count; i++)
list[i].box = bestBoxes[i];
}
////Testing whether first strategy is always better when no nulls are found
//if (nullPositionsPerStrategy[0] == 0 && iBest != 0)
// throw new System.Exception(nullPositionsPerStrategy[0] + ", " + (Packer2DBox.NodeChoosingStrategy)iBest + ", " + nullPositionsPerStrategy[iBest]);
//Debug.Log("Strategy used: " + (Packer2DBox.NodeChoosingStrategy)iBest);
width = (float)totalWidthsPerStrategy[iBest];
height = (float)totalHeightsPerStrategy[iBest];
return list;
}
public enum AxisOrNone
{
Horizontal = RectTransform.Axis.Horizontal,
Vertical = RectTransform.Axis.Vertical,
None
}
class ChildSetup
{
public RectTransform child;
public Packer2DBox.Box box;
public ChildSetup(double width, double height, RectTransform child)
{
this.child = child;
box = new Packer2DBox.Box(width, height);
}
public void ReinitBox() { box = new Packer2DBox.Box(box.width, box.height); }
}
}
}