Um algoritmo para espaçar retângulos sobrepostos?

92

Este problema realmente lida com roll-overs, generalizarei a seguir como tal:

Tenho uma visualização 2D e vários retângulos em uma área da tela. Como faço para espalhar essas caixas de forma que elas não se sobreponham, mas apenas as ajuste com o mínimo de movimento?

As posições dos retângulos são dinâmicas e dependem da entrada do usuário, portanto, suas posições podem estar em qualquer lugar.

As texto alternativoimagens anexadas mostram o problema e a solução desejada

O problema da vida real lida com rollovers, na verdade.

Respostas às perguntas nos comentários

  1. O tamanho dos retângulos não é fixo e depende do comprimento do texto no rollover

  2. Sobre o tamanho da tela, agora acho melhor assumir que o tamanho da tela é suficiente para os retângulos. Se houver muitos retângulos e o algo não produzir solução, então só preciso ajustar o conteúdo.

  3. O requisito de 'mover-se minimamente' é mais para a estética do que um requisito absoluto de engenharia. Pode-se espaçar dois retângulos adicionando uma grande distância entre eles, mas não ficará bem como parte da GUI. A ideia é obter o rollover / retângulo o mais próximo possível de sua fonte (que irei conectar à fonte com uma linha preta). Portanto, 'movendo apenas um para x' ou 'movendo ambos para meio x' está bom.

Extrakun
fonte
2
Podemos assumir que os retângulos são sempre orientados horizontalmente ou verticalmente, e não inclinados em seu eixo em um ângulo?
Matt
2
Sim, a suposição é válida.
Extrakun
Podemos assumir que a tela é sempre grande o suficiente para suportar os retângulos sem sobreposição? Os retângulos são sempre do mesmo tamanho? Você pode ser mais específico sobre o que significa "movimento mínimo"? Por exemplo, se você tiver 2 retângulos exatamente um em cima do outro, é melhor apenas 1 deles a distância total para remover a sobreposição, ou mover ambos a metade da distância?
Nick Larsen
@NickLarsen, respondi às suas perguntas na resposta editada acima. Obrigado!
Extrakun
1
@joe: talvez ele queira entender a solução, para que possa apoiá-la.
Beska

Respostas:

95

Eu estava trabalhando um pouco nisso, pois também precisava de algo semelhante, mas atrasei o desenvolvimento do algoritmo. Você me ajudou a obter algum impulso: D

Eu também precisava do código-fonte, então aqui está. Eu trabalhei no Mathematica, mas como não usei muito os recursos funcionais, acho que será fácil traduzir para qualquer linguagem procedural.

Uma perspectiva histórica

Primeiro decidi desenvolver o algoritmo para círculos, porque a interseção é mais fácil de calcular. Depende apenas dos centros e raios.

Consegui usar o solucionador de equações do Mathematica e ele teve um bom desempenho.

Apenas olhe:

texto alternativo

Foi fácil. Acabei de carregar o solucionador com o seguinte problema:

For each circle
 Solve[
  Find new coördinates for the circle
  Minimizing the distance to the geometric center of the image
  Taking in account that
      Distance between centers > R1+R2 *for all other circles
      Move the circle in a line between its center and the 
                                         geometric center of the drawing
   ]

Tão simples quanto isso, e o Mathematica fez todo o trabalho.

Eu disse "Ha! É fácil, agora vamos para os retângulos!". Mas eu estava errado ...

Rectangular Blues

O principal problema com os retângulos é que consultar a interseção é uma função desagradável. Algo como:

Então, quando tentei alimentar o Mathematica com várias dessas condições para a equação, ela se saiu tão mal que resolvi fazer algo processual.

Meu algoritmo acabou da seguinte maneira:

Expand each rectangle size by a few points to get gaps in final configuration
While There are intersections
    sort list of rectangles by number of intersections
    push most intersected rectangle on stack, and remove it from list
// Now all remaining rectangles doesn't intersect each other
While stack not empty
    pop  rectangle from stack and re-insert it into list
    find the geometric center G of the chart (each time!)
    find the movement vector M (from G to rectangle center)
    move the rectangle incrementally in the direction of M (both sides) 
                                                 until no intersections  
Shrink the rectangles to its original size

Você pode notar que a condição de "menor movimento" não é completamente satisfeita (apenas em uma direção). Mas descobri que mover os retângulos em qualquer direção para satisfazê-lo, às vezes resulta em uma mudança de mapa confusa para o usuário.

Como estou projetando uma interface de usuário, escolho mover o retângulo um pouco mais longe, mas de uma forma mais previsível. Você pode alterar o algoritmo para inspecionar todos os ângulos e raios ao redor de sua posição atual até que um lugar vazio seja encontrado, embora seja muito mais exigente.

De qualquer forma, estes são exemplos de resultados (antes / depois):

texto alternativo

Editar> Mais exemplos aqui

Como você pode ver, o "movimento mínimo" não é satisfeito, mas os resultados são bons o suficiente.

Vou postar o código aqui porque estou tendo alguns problemas com meu repositório SVN. Vou removê-lo quando os problemas forem resolvidos.

Editar:

Você também pode usar R-Trees para encontrar interseções retangulares, mas parece um exagero lidar com um pequeno número de retângulos. E eu não tenho os algoritmos já implementados. Talvez outra pessoa possa indicar uma implementação existente na plataforma de sua escolha.

Aviso! O código é uma primeira abordagem ... não é de grande qualidade ainda e certamente tem alguns bugs.

É o Mathematica.

(*Define some functions first*)

Clear["Global`*"];
rn[x_] := RandomReal[{0, x}];
rnR[x_] := RandomReal[{1, x}];
rndCol[] := RGBColor[rn[1], rn[1], rn[1]];

minX[l_, i_] := l[[i]][[1]][[1]]; (*just for easy reading*)
maxX[l_, i_] := l[[i]][[1]][[2]];
minY[l_, i_] := l[[i]][[2]][[1]];
maxY[l_, i_] := l[[i]][[2]][[2]];
color[l_, i_]:= l[[i]][[3]];

intersectsQ[l_, i_, j_] := (* l list, (i,j) indexes, 
                              list={{x1,x2},{y1,y2}} *) 
                           (*A rect does intesect with itself*)
          If[Max[minX[l, i], minX[l, j]] < Min[maxX[l, i], maxX[l, j]] &&
             Max[minY[l, i], minY[l, j]] < Min[maxY[l, i], maxY[l, j]], 
                                                           True,False];

(* Number of Intersects for a Rectangle *)
(* With i as index*)
countIntersects[l_, i_] := 
          Count[Table[intersectsQ[l, i, j], {j, 1, Length[l]}], True]-1;

(*And With r as rectangle *)
countIntersectsR[l_, r_] := (
    Return[Count[Table[intersectsQ[Append[l, r], Length[l] + 1, j], 
                       {j, 1, Length[l] + 1}], True] - 2];)

(* Get the maximum intersections for all rectangles*)
findMaxIntesections[l_] := Max[Table[countIntersects[l, i], 
                                       {i, 1, Length[l]}]];

(* Get the rectangle center *)
rectCenter[l_, i_] := {1/2 (maxX[l, i] + minX[l, i] ), 
                       1/2 (maxY[l, i] + minY[l, i] )};

(* Get the Geom center of the whole figure (list), to move aesthetically*)
geometryCenter[l_] :=  (* returs {x,y} *)
                      Mean[Table[rectCenter[l, i], {i, Length[l]}]]; 

(* Increment or decr. size of all rects by a bit (put/remove borders)*)
changeSize[l_, incr_] :=
                 Table[{{minX[l, i] - incr, maxX[l, i] + incr},
                        {minY[l, i] - incr, maxY[l, i] + incr},
                        color[l, i]},
                        {i, Length[l]}];

sortListByIntersections[l_] := (* Order list by most intersecting Rects*)
        Module[{a, b}, 
               a = MapIndexed[{countIntersectsR[l, #1], #2} &, l];
               b = SortBy[a, -#[[1]] &];
               Return[Table[l[[b[[i]][[2]][[1]]]], {i, Length[b]}]];
        ];

(* Utility Functions*)
deb[x_] := (Print["--------"]; Print[x]; Print["---------"];)(* for debug *)
tableForPlot[l_] := (*for plotting*)
                Table[{color[l, i], Rectangle[{minX[l, i], minY[l, i]},
                {maxX[l, i], maxY[l, i]}]}, {i, Length[l]}];

genList[nonOverlap_, Overlap_] :=    (* Generate initial lists of rects*)
      Module[{alist, blist, a, b}, 
          (alist = (* Generate non overlapping - Tabuloid *)
                Table[{{Mod[i, 3], Mod[i, 3] + .8}, 
                       {Mod[i, 4], Mod[i, 4] + .8},  
                       rndCol[]}, {i, nonOverlap}];
           blist = (* Random overlapping *)
                Table[{{a = rnR[3], a + rnR[2]}, {b = rnR[3], b + rnR[2]}, 
                      rndCol[]}, {Overlap}];
           Return[Join[alist, blist] (* Join both *)];)
      ];

a Principal

clist = genList[6, 4]; (* Generate a mix fixed & random set *)

incr = 0.05; (* may be some heuristics needed to determine best increment*)

clist = changeSize[clist,incr]; (* expand rects so that borders does not 
                                                         touch each other*)

(* Now remove all intercepting rectangles until no more intersections *)

workList = {}; (* the stack*)

While[findMaxIntesections[clist] > 0,          
                                      (*Iterate until no intersections *)
    clist    = sortListByIntersections[clist]; 
                                      (*Put the most intersected first*)
    PrependTo[workList, First[clist]];         
                                      (* Push workList with intersected *)
    clist    = Delete[clist, 1];      (* and Drop it from clist *)
];

(* There are no intersections now, lets pop the stack*)

While [workList != {},

    PrependTo[clist, First[workList]];       
                                 (*Push first element in front of clist*)
    workList = Delete[workList, 1];          
                                 (* and Drop it from worklist *)

    toMoveIndex = 1;                        
                                 (*Will move the most intersected Rect*)
    g = geometryCenter[clist];               
                                 (*so the geom. perception is preserved*)
    vectorToMove = rectCenter[clist, toMoveIndex] - g;
    If [Norm[vectorToMove] < 0.01, vectorToMove = {1,1}]; (*just in case*)  
    vectorToMove = vectorToMove/Norm[vectorToMove];      
                                            (*to manage step size wisely*)

    (*Now iterate finding minimum move first one way, then the other*)

    i = 1; (*movement quantity*)

    While[countIntersects[clist, toMoveIndex] != 0, 
                                           (*If the Rect still intersects*)
                                           (*move it alternating ways (-1)^n *)

      clist[[toMoveIndex]][[1]] += (-1)^i i incr vectorToMove[[1]];(*X coords*)
      clist[[toMoveIndex]][[2]] += (-1)^i i incr vectorToMove[[2]];(*Y coords*)

            i++;
    ];
];
clist = changeSize[clist, -incr](* restore original sizes*);

HTH!

Editar: Pesquisa em vários ângulos

Implementei uma mudança no algoritmo permitindo pesquisar em todas as direções, mas dando preferência ao eixo imposto pela simetria geométrica.
À custa de mais ciclos, isso resultou em configurações finais mais compactas, como você pode ver aqui abaixo:

insira a descrição da imagem aqui

Mais amostras aqui .

O pseudocódigo do loop principal mudou para:

Expand each rectangle size by a few points to get gaps in final configuration
While There are intersections
    sort list of rectangles by number of intersections
    push most intersected rectangle on stack, and remove it from list
// Now all remaining rectangles doesn't intersect each other
While stack not empty
    find the geometric center G of the chart (each time!)
    find the PREFERRED movement vector M (from G to rectangle center)
    pop  rectangle from stack 
    With the rectangle
         While there are intersections (list+rectangle)
              For increasing movement modulus
                 For increasing angle (0, Pi/4)
                    rotate vector M expanding the angle alongside M
                    (* angle, -angle, Pi + angle, Pi-angle*)
                    re-position the rectangle accorging to M
    Re-insert modified vector into list
Shrink the rectangles to its original size

Não estou incluindo o código-fonte para abreviar, mas pergunte se você acha que pode usá-lo. Eu acho que, se você seguir este caminho, é melhor mudar para R-trees (muitos testes de intervalo necessários aqui)

belisarius
fonte
4
Agradável. Meu amigo e eu estamos tentando implementá-lo. dedos cruzados Obrigado pelo tempo para colocar isso!
Extrakun
9
Explicando o processo de pensamento, conceito de algoritmo, dificuldades e limitações e fornecendo código == +1. E mais se eu pudesse oferecer.
Beska
1
@belisarlus Ótimo, escreva! Você já tornou sua fonte pública?
Rohan West
Existem outras respostas aqui que tentam responder a isso de uma forma java. Alguém conseguiu portar esta solução mathematica para java?
mainstringargs
11

Aqui está um palpite.

Encontre o centro C da caixa delimitadora de seus retângulos.

Para cada retângulo R que se sobrepõe a outro.

  1. Defina um vetor de movimento v.
  2. Encontre todos os retângulos R 'que se sobrepõem a R.
  3. Adicione um vetor av proporcional ao vetor entre o centro de R e R '.
  4. Adicione um vetor av proporcional ao vetor entre C e o centro de R.
  5. Mova R por v.
  6. Repita até que nada se sobreponha.

Isso move gradualmente os retângulos para longe uns dos outros e do centro de todos os retângulos. Isso terminará porque o componente de v da etapa 4 eventualmente os espalhará o suficiente por si só.

cape1232
fonte
É uma boa ideia encontrar o centro e mover os retângulos em torno dele. +1 O único problema é que encontrar o centro é outro problema por si só, e provavelmente muito mais desafiador para cada retângulo adicionado.
Nick Larsen
2
Encontrar o centro é fácil. Basta pegar o mínimo e o máximo dos cantos de todos os retângulos. E você só faz isso uma vez, não uma vez por iteração.
cape1232
Isso resulta em um movimento mínimo também, no sentido de que não move um retângulo se nada se sobrepõe a ele. Ah, a etapa 4 sim, então você deve pular a etapa 4 se não houver sobreposições. Encontrar o arranjo real que requer movimento mínimo é provavelmente muito mais difícil.
cape1232
Para dois retângulos localizados em um canto da área visível, o alg deve ser capaz de entender se o gráfico deve ser expandido ou contraído. Apenas reclamando. (Eu sei que a visibilidade ainda não está no escopo, mas acho que é importante não resolver o problema apenas expandindo o gráfico o suficiente, porque senão a solução é trivial: pegue os dois quadrados mais próximos e "irradie" todo o gráfico de seu centro de massa o suficiente para separar esses dois retângulos). Sua abordagem é melhor do que isso, é claro. Estou apenas dizendo que não devemos expandir a menos que seja necessário.
Dr. belisarius
@belisarius Não se expande se não for necessário. Quando nada se sobrepõe ao retângulo, ele para de se mover. (Pode começar de novo, mas apenas quando for necessário.) Com retângulos suficientes ou grandes o suficiente, pode não ser possível mostrá-los todos na tela em tamanho real. Nesse caso, é fácil encontrar a caixa delimitadora da solução redimensionada e dimensionar tudo da mesma forma para que caibam na tela.
cape1232
6

Acho que essa solução é bem parecida com a do cape1232, mas já está implementada, vale a pena conferir :)

Siga para esta discussão do reddit: http://www.reddit.com/r/gamedev/comments/1dlwc4/procedural_dungeon_generation_algorithm_explained/ e verifique a descrição e implementação. Não há código-fonte disponível, então aqui está minha abordagem para esse problema no AS3 (funciona exatamente da mesma forma, mas mantém os retângulos ajustados à resolução da grade):

public class RoomSeparator extends AbstractAction {
    public function RoomSeparator(name:String = "Room Separator") {
        super(name);
    }

    override public function get finished():Boolean { return _step == 1; }

    override public function step():void {
        const repelDecayCoefficient:Number = 1.0;

        _step = 1;

        var count:int = _activeRoomContainer.children.length;
        for(var i:int = 0; i < count; i++) {
            var room:Room           = _activeRoomContainer.children[i];
            var center:Vector3D     = new Vector3D(room.x + room.width / 2, room.y + room.height / 2);
            var velocity:Vector3D   = new Vector3D();

            for(var j:int = 0; j < count; j++) {
                if(i == j)
                    continue;

                var otherRoom:Room = _activeRoomContainer.children[j];
                var intersection:Rectangle = GeomUtil.rectangleIntersection(room.createRectangle(), otherRoom.createRectangle());

                if(intersection == null || intersection.width == 0 || intersection.height == 0)
                    continue;

                var otherCenter:Vector3D = new Vector3D(otherRoom.x + otherRoom.width / 2, otherRoom.y + otherRoom.height / 2);
                var diff:Vector3D = center.subtract(otherCenter);

                if(diff.length > 0) {
                    var scale:Number = repelDecayCoefficient / diff.lengthSquared;
                    diff.normalize();
                    diff.scaleBy(scale);

                    velocity = velocity.add(diff);
                }
            }

            if(velocity.length > 0) {
                _step = 0;
                velocity.normalize();

                room.x += Math.abs(velocity.x) < 0.5 ? 0 : velocity.x > 0 ? _resolution : -_resolution;
                room.y += Math.abs(velocity.y) < 0.5 ? 0 : velocity.y > 0 ? _resolution : -_resolution;
            }
        }
    }
}
b005t3r
fonte
Existe uma falha na lógica. Quanto a uma sala, velocityé a soma dos vetores entre seu centro e o centro das outras salas, se todas as salas estiverem empilhadas com o mesmo centro, velocity.length == 0para todas as salas e nada jamais se moverá. Da mesma forma, se duas ou mais salas tiverem o mesmo retângulo com o mesmo centro, elas se moverão juntas, mas permanecerão empilhadas.
Peyre
6

Eu realmente gosto da implementação do b005t3r! Funciona em meus casos de teste, no entanto, meu representante é muito baixo para deixar um comentário com as 2 correções sugeridas.

  1. Você não deve traduzir quartos por incrementos de resolução simples, você deve traduzir pela velocidade que você calculou de forma dolorosa! Isso torna a separação mais orgânica, pois salas com interseção profunda separam mais cada iteração do que salas com interseção não tão profunda.

  2. Você não deve assumir que velociites menores que 0,5 significa que os quartos são separados, pois você pode ficar preso em um caso em que nunca está separado. Imagine que 2 quartos se cruzam, mas são incapazes de se corrigirem porque sempre que um deles tenta corrigir a penetração, eles calculam a velocidade necessária como <0,5, de modo que iteram indefinidamente.

Aqui está uma solução Java (: Cheers!

do {
    _separated = true;

    for (Room room : getRooms()) {
        // reset for iteration
        Vector2 velocity = new Vector2();
        Vector2 center = room.createCenter();

        for (Room other_room : getRooms()) {
            if (room == other_room)
                continue;

            if (!room.createRectangle().overlaps(other_room.createRectangle()))
                continue;

            Vector2 other_center = other_room.createCenter();
            Vector2 diff = new Vector2(center.x - other_center.x, center.y - other_center.y);
            float diff_len2 = diff.len2();

            if (diff_len2 > 0f) {
                final float repelDecayCoefficient = 1.0f;
                float scale = repelDecayCoefficient / diff_len2;
                diff.nor();
                diff.scl(scale);

                velocity.add(diff);
            }
        }

        if (velocity.len2() > 0f) {
            _separated = false;

            velocity.nor().scl(delta * 20f);

            room.getPosition().add(velocity);
        }
    }
} while (!_separated);
Cord Rehn
fonte
4

Aqui está um algoritmo escrito em Java para lidar com um cluster de Rectangles não rotacionados . Permite especificar a relação de aspecto desejada do layout e posicionar o cluster usando um parâmetro parametrizado Rectanglecomo ponto de ancoragem, sobre o qual todas as traduções feitas são orientadas. Você também pode especificar uma quantidade arbitrária de preenchimento pelo qual gostaria de espalhar os Rectangles.

public final class BoxxyDistribution {

/* Static Definitions. */
private static final int INDEX_BOUNDS_MINIMUM_X = 0;
private static final int INDEX_BOUNDS_MINIMUM_Y = 1;
private static final int INDEX_BOUNDS_MAXIMUM_X = 2;
private static final int INDEX_BOUNDS_MAXIMUM_Y = 3;

private static final double onCalculateMagnitude(final double pDeltaX, final double pDeltaY) {
    return Math.sqrt((pDeltaX * pDeltaX) + (pDeltaY + pDeltaY));
}

/* Updates the members of EnclosingBounds to ensure the dimensions of T can be completely encapsulated. */
private static final void onEncapsulateBounds(final double[] pEnclosingBounds, final double pMinimumX, final double pMinimumY, final double pMaximumX, final double pMaximumY) {
    pEnclosingBounds[0] = Math.min(pEnclosingBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_X], pMinimumX);
    pEnclosingBounds[1] = Math.min(pEnclosingBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_Y], pMinimumY);
    pEnclosingBounds[2] = Math.max(pEnclosingBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_X], pMaximumX);
    pEnclosingBounds[3] = Math.max(pEnclosingBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_Y], pMaximumY);
}

private static final void onEncapsulateBounds(final double[] pEnclosingBounds, final double[] pBounds) {
    BoxxyDistribution.onEncapsulateBounds(pEnclosingBounds, pBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_X], pBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_Y], pBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_X], pBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_Y]);
}

private static final double onCalculateMidpoint(final double pMaximum, final double pMinimum) {
    return ((pMaximum - pMinimum) * 0.5) + pMinimum;
}

/* Re-arranges a List of Rectangles into something aesthetically pleasing. */
public static final void onBoxxyDistribution(final List<Rectangle> pRectangles, final Rectangle pAnchor, final double pPadding, final double pAspectRatio, final float pRowFillPercentage) {
    /* Create a safe clone of the Rectangles that we can modify as we please. */
    final List<Rectangle> lRectangles  = new ArrayList<Rectangle>(pRectangles);
    /* Allocate a List to track the bounds of each Row. */
    final List<double[]>  lRowBounds   = new ArrayList<double[]>(); // (MinX, MinY, MaxX, MaxY)
    /* Ensure Rectangles does not contain the Anchor. */
    lRectangles.remove(pAnchor);
    /* Order the Rectangles via their proximity to the Anchor. */
    Collections.sort(pRectangles, new Comparator<Rectangle>(){ @Override public final int compare(final Rectangle pT0, final Rectangle pT1) {
        /* Calculate the Distance for pT0. */
        final double lDistance0 = BoxxyDistribution.onCalculateMagnitude(pAnchor.getCenterX() - pT0.getCenterX(), pAnchor.getCenterY() - pT0.getCenterY());
        final double lDistance1 = BoxxyDistribution.onCalculateMagnitude(pAnchor.getCenterX() - pT1.getCenterX(), pAnchor.getCenterY() - pT1.getCenterY());
        /* Compare the magnitude in distance between the anchor and the Rectangles. */
        return Double.compare(lDistance0, lDistance1);
    } });
    /* Initialize the RowBounds using the Anchor. */ /** TODO: Probably better to call getBounds() here. **/
    lRowBounds.add(new double[]{ pAnchor.getX(), pAnchor.getY(), pAnchor.getX() + pAnchor.getWidth(), pAnchor.getY() + pAnchor.getHeight() });

    /* Allocate a variable for tracking the TotalBounds of all rows. */
    final double[] lTotalBounds = new double[]{ Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY };
    /* Now we iterate the Rectangles to place them optimally about the Anchor. */
    for(int i = 0; i < lRectangles.size(); i++) {
        /* Fetch the Rectangle. */
        final Rectangle lRectangle = lRectangles.get(i);
        /* Iterate through each Row. */
        for(final double[] lBounds : lRowBounds) {
            /* Update the TotalBounds. */
            BoxxyDistribution.onEncapsulateBounds(lTotalBounds, lBounds);
        }
        /* Allocate a variable to state whether the Rectangle has been allocated a suitable RowBounds. */
        boolean lIsBounded = false;
        /* Calculate the AspectRatio. */
        final double lAspectRatio = (lTotalBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_X] - lTotalBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_X]) / (lTotalBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_Y] - lTotalBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_Y]);
        /* We will now iterate through each of the available Rows to determine if a Rectangle can be stored. */
        for(int j = 0; j < lRowBounds.size() && !lIsBounded; j++) {
            /* Fetch the Bounds. */
            final double[] lBounds = lRowBounds.get(j);
            /* Calculate the width and height of the Bounds. */
            final double   lWidth  = lBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_X] - lBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_X];
            final double   lHeight = lBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_Y] - lBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_Y];
            /* Determine whether the Rectangle is suitable to fit in the RowBounds. */
            if(lRectangle.getHeight() <= lHeight && !(lAspectRatio > pAspectRatio && lWidth > pRowFillPercentage * (lTotalBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_X] - lTotalBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_X]))) {
                /* Register that the Rectangle IsBounded. */
                lIsBounded = true;
                /* Update the Rectangle's X and Y Co-ordinates. */
                lRectangle.setFrame((lRectangle.getX() > BoxxyDistribution.onCalculateMidpoint(lBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_X], lBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_X])) ? lBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_X] + pPadding : lBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_X] - (pPadding + lRectangle.getWidth()), lBounds[1], lRectangle.getWidth(), lRectangle.getHeight());
                /* Update the Bounds. (Do not modify the vertical metrics.) */
                BoxxyDistribution.onEncapsulateBounds(lTotalBounds, lRectangle.getX(), lBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_Y], lRectangle.getX() + lRectangle.getWidth(), lBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_Y] + lHeight);
            }
        }
        /* Determine if the Rectangle has not been allocated a Row. */
        if(!lIsBounded) {
            /* Calculate the MidPoint of the TotalBounds. */
            final double lCentreY   = BoxxyDistribution.onCalculateMidpoint(lTotalBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_Y], lTotalBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_Y]);
            /* Determine whether to place the bounds above or below? */
            final double lYPosition = lRectangle.getY() < lCentreY ? lTotalBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_Y] - (pPadding + lRectangle.getHeight()) : (lTotalBounds[BoxxyDistribution.INDEX_BOUNDS_MAXIMUM_Y] + pPadding);
            /* Create a new RowBounds. */
            final double[] lBounds  = new double[]{ pAnchor.getX(), lYPosition, pAnchor.getX() + lRectangle.getWidth(), lYPosition + lRectangle.getHeight() };
            /* Allocate a new row, roughly positioned about the anchor. */
            lRowBounds.add(lBounds);
            /* Position the Rectangle. */
            lRectangle.setFrame(lBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_X], lBounds[BoxxyDistribution.INDEX_BOUNDS_MINIMUM_Y], lRectangle.getWidth(), lRectangle.getHeight());
        }
    }
}

}

Aqui está um exemplo usando um AspectRatiode 1.2, um FillPercentagede 0.8e um Paddingde 10.0.

100 retângulos escalonados e distribuídos aleatoriamente.

Os 100 retângulos aleatórios distribuídos usando o BoxxyDistribution.

Esta é uma abordagem determinística que permite que o espaçamento ocorra ao redor da âncora, enquanto deixa a localização da própria âncora inalterada. Isso permite que o layout ocorra onde quer que esteja o Ponto de Interesse do usuário. A lógica para selecionar uma posição é muito simplista, mas acho que a arquitetura envolvente de classificar os elementos com base em sua posição inicial e, em seguida, iterá-los é uma abordagem útil para implementar uma distribuição relativamente previsível. Além disso, não estamos contando com testes de interseção iterativos ou qualquer coisa assim, apenas construindo algumas caixas delimitadoras para nos dar uma indicação ampla de onde alinhar as coisas; depois disso, a aplicação de preenchimento vem naturalmente.

Mapsy
fonte
3

Esta é uma versão que segue a resposta de cape1232 e é um exemplo executável autônomo para Java:

public class Rectangles extends JPanel {

    List<Rectangle2D> rectangles = new ArrayList<Rectangle2D>();
    {
        // x,y,w,h
        rectangles.add(new Rectangle2D.Float(300, 50, 50, 50));

        rectangles.add(new Rectangle2D.Float(300, 50, 20, 50));

        rectangles.add(new Rectangle2D.Float(100, 100, 100, 50));

        rectangles.add(new Rectangle2D.Float(120, 200, 50, 50));

        rectangles.add(new Rectangle2D.Float(150, 130, 100, 100));

        rectangles.add(new Rectangle2D.Float(0, 100, 100, 50));

        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < 10; j++) {
                rectangles.add(new Rectangle2D.Float(i * 40, j * 40, 20, 20));
            }
        }
    }

    List<Rectangle2D> rectanglesToDraw;

    protected void reset() {
        rectanglesToDraw = rectangles;

        this.repaint();
    }

    private List<Rectangle2D> findIntersections(Rectangle2D rect, List<Rectangle2D> rectList) {

        ArrayList<Rectangle2D> intersections = new ArrayList<Rectangle2D>();

        for (Rectangle2D intersectingRect : rectList) {
            if (!rect.equals(intersectingRect) && intersectingRect.intersects(rect)) {
                intersections.add(intersectingRect);
            }
        }

        return intersections;
    }

    protected void fix() {
        rectanglesToDraw = new ArrayList<Rectangle2D>();

        for (Rectangle2D rect : rectangles) {
            Rectangle2D copyRect = new Rectangle2D.Double();
            copyRect.setRect(rect);
            rectanglesToDraw.add(copyRect);
        }

        // Find the center C of the bounding box of your rectangles.
        Rectangle2D surroundRect = surroundingRect(rectanglesToDraw);
        Point center = new Point((int) surroundRect.getCenterX(), (int) surroundRect.getCenterY());

        int movementFactor = 5;

        boolean hasIntersections = true;

        while (hasIntersections) {

            hasIntersections = false;

            for (Rectangle2D rect : rectanglesToDraw) {

                // Find all the rectangles R' that overlap R.
                List<Rectangle2D> intersectingRects = findIntersections(rect, rectanglesToDraw);

                if (intersectingRects.size() > 0) {

                    // Define a movement vector v.
                    Point movementVector = new Point(0, 0);

                    Point centerR = new Point((int) rect.getCenterX(), (int) rect.getCenterY());

                    // For each rectangle R that overlaps another.
                    for (Rectangle2D rPrime : intersectingRects) {
                        Point centerRPrime = new Point((int) rPrime.getCenterX(), (int) rPrime.getCenterY());

                        int xTrans = (int) (centerR.getX() - centerRPrime.getX());
                        int yTrans = (int) (centerR.getY() - centerRPrime.getY());

                        // Add a vector to v proportional to the vector between the center of R and R'.
                        movementVector.translate(xTrans < 0 ? -movementFactor : movementFactor,
                                yTrans < 0 ? -movementFactor : movementFactor);

                    }

                    int xTrans = (int) (centerR.getX() - center.getX());
                    int yTrans = (int) (centerR.getY() - center.getY());

                    // Add a vector to v proportional to the vector between C and the center of R.
                    movementVector.translate(xTrans < 0 ? -movementFactor : movementFactor,
                            yTrans < 0 ? -movementFactor : movementFactor);

                    // Move R by v.
                    rect.setRect(rect.getX() + movementVector.getX(), rect.getY() + movementVector.getY(),
                            rect.getWidth(), rect.getHeight());

                    // Repeat until nothing overlaps.
                    hasIntersections = true;
                }

            }
        }
        this.repaint();
    }

    private Rectangle2D surroundingRect(List<Rectangle2D> rectangles) {

        Point topLeft = null;
        Point bottomRight = null;

        for (Rectangle2D rect : rectangles) {
            if (topLeft == null) {
                topLeft = new Point((int) rect.getMinX(), (int) rect.getMinY());
            } else {
                if (rect.getMinX() < topLeft.getX()) {
                    topLeft.setLocation((int) rect.getMinX(), topLeft.getY());
                }

                if (rect.getMinY() < topLeft.getY()) {
                    topLeft.setLocation(topLeft.getX(), (int) rect.getMinY());
                }
            }

            if (bottomRight == null) {
                bottomRight = new Point((int) rect.getMaxX(), (int) rect.getMaxY());
            } else {
                if (rect.getMaxX() > bottomRight.getX()) {
                    bottomRight.setLocation((int) rect.getMaxX(), bottomRight.getY());
                }

                if (rect.getMaxY() > bottomRight.getY()) {
                    bottomRight.setLocation(bottomRight.getX(), (int) rect.getMaxY());
                }
            }
        }

        return new Rectangle2D.Double(topLeft.getX(), topLeft.getY(), bottomRight.getX() - topLeft.getX(),
                bottomRight.getY() - topLeft.getY());
    }

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g;

        for (Rectangle2D entry : rectanglesToDraw) {
            g2d.setStroke(new BasicStroke(1));
            // g2d.fillRect((int) entry.getX(), (int) entry.getY(), (int) entry.getWidth(),
            // (int) entry.getHeight());
            g2d.draw(entry);
        }

    }

    protected static void createAndShowGUI() {
        Rectangles rects = new Rectangles();

        rects.reset();

        JFrame frame = new JFrame("Rectangles");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLayout(new BorderLayout());
        frame.add(rects, BorderLayout.CENTER);

        JPanel buttonsPanel = new JPanel();

        JButton fix = new JButton("Fix");

        fix.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                rects.fix();

            }
        });

        JButton resetButton = new JButton("Reset");

        resetButton.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                rects.reset();
            }
        });

        buttonsPanel.add(fix);
        buttonsPanel.add(resetButton);

        frame.add(buttonsPanel, BorderLayout.SOUTH);

        frame.setSize(400, 400);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                createAndShowGUI();

            }
        });
    }

}
mainstringargs
fonte