Drawing the outermost boundaries of set of squares - algorithm

I need to draw a border in Unity. I have some randomly generated squares, each of them at least neighbour to another square. I'll use some kind of line renderer, so I need to give line renderer points' coordinates which will create the border.
Figure one is my squares, Figure two is expected red border. In figure three, I tried to explain what I need, points in an order which will generate the red border around those squares.
I know each boxes world position exactly. So what I'm looking is an algorithm to move from point a to b, to create that border.
Any pseudo algorithm will be helpful.
(I will use it in an turn based rpg, where I want to highlight the area which a character can move. I dont want to highlight the whole squares as area, but only the border. Like its in XCOM games)

EDIT :
I didn't realized that an ordered list of coordinates was required, my original answer was making a non oriented graph around the shape.
I updated the answer to add a function that would build an oriented list from this graph
Considering you're using Unity, I made a very simple algorithm.
Knowing only the coordinates of every vertices that is part of the figure you want to outline, and with the assumption that the distance between them is always the same, I came up with this :
Firstly a very crude way of making the squares :
The assumption is that I the coordinates have no concept of what are their edges, only the four points of each square are given.
For the sake of this example I've done this handcrafting a list of list of vertex.
public void CreateCoords()
{
_squares = new List<List<Vector3>>()
{
new List<Vector3>()
{
new Vector3(0,0,0),
new Vector3(0,0,1),
new Vector3(1,0,0),
new Vector3(1,0,1),
},
new List<Vector3>()
{
new Vector3(0,0,1),
new Vector3(1,0,1),
new Vector3(1,0,2),
new Vector3(0,0,2),
},
new List<Vector3>()
{
new Vector3(1,0,1),
new Vector3(2,0,1),
new Vector3(2,0,2),
new Vector3(1,0,2),
},
new List<Vector3>()
{
new Vector3(2,0,1),
new Vector3(2,0,2),
new Vector3(3,0,1),
new Vector3(3,0,2),
},
new List<Vector3>()
{
new Vector3(2,0,1),
new Vector3(3,0,1),
new Vector3(2,0,0),
new Vector3(3,0,0),
},
new List<Vector3>()
{
new Vector3(4,0,1),
new Vector3(3,0,1),
new Vector3(4,0,0),
new Vector3(3,0,0),
},
new List<Vector3>()
{
new Vector3(1,0,2),
new Vector3(1,0,3),
new Vector3(2,0,2),
new Vector3(2,0,3),
},
new List<Vector3>()
{
new Vector3(3,0,2),
new Vector3(3,0,3),
new Vector3(2,0,2),
new Vector3(2,0,3),
},
new List<Vector3>()
{
new Vector3(3,0,4),
new Vector3(3,0,3),
new Vector3(2,0,4),
new Vector3(2,0,3),
},
new List<Vector3>()
{
new Vector3(1,0,4),
new Vector3(1,0,3),
new Vector3(2,0,4),
new Vector3(2,0,3),
},
new List<Vector3>()
{
new Vector3(3,0,4),
new Vector3(3,0,5),
new Vector3(2,0,4),
new Vector3(2,0,5),
},
new List<Vector3>()
{
new Vector3(3,0,2),
new Vector3(3,0,3),
new Vector3(4,0,2),
new Vector3(4,0,3),
},
};
}
Here's the result :
Now for the algorithm :
My solution is based on the fact that all edges that are part of the border will only appear once.
It also works on the assumption that the coordinates of the squares are not ordered, the algorithm would be a bit simpler if it was the case.
private void ComputeEdges()
{
// The edges collection is a set of pair of vertex
_edges = new HashSet<KeyValuePair<Vector3, Vector3>>();
foreach (var square in _squares)
{
// Iterate over the coordinates to compute the edges
// Using for loop to skip already processed edges
var squareCount = square.Count;
for (var i = 0; i < squareCount; i++)
{
// The source vertex
var src = square[i];
for (var j = 0; j < squareCount; j++)
{
if (i == j) continue;
// The vertex with whom we want to determine if they form and edge
var dest = square[j];
// Check the distance between them to filter out the diagonal edges
if (!(Math.Abs(Vector3.Distance(src, dest) - edgeDistance) < 0.001)) continue;
var edge = new KeyValuePair<Vector3, Vector3>(src, dest);
// _edges is a set, making it viable to use Contains
// even when the collections contains a lot of elements
if (_edges.Contains(edge))
{
// If the edge already exists in the set,
// it means its not part of the border
_edges.Remove(edge);
}
else
{
_edges.Add(edge);
}
}
}
}
}
Here's a simple Gizmos example that will display the borders.
foreach (var (src, dest) in _edges)
{
Gizmos.DrawLine(src, dest);
}
And here's the final result :
As I understood later that an ordered list of points would be prefered, I made another algorithm that could make one out of the edges collection.
public void BuildList()
{
_coordsList = new List<Vector3>();
// Make a copy of the edges so we can remove items from it
// without destroying the original collection
var copy = new HashSet<KeyValuePair<Vector3, Vector3>>(_edges);
// Add the first pair before starting the loop
var previousEdge = _edges.First();
_coordsList.Add(previousEdge.Key);
_coordsList.Add(previousEdge.Value);
KeyValuePair<Vector3, Vector3> currentEdge;
// While there is an edge that follows the previous one
while (!(currentEdge = copy.FirstOrDefault(pair => pair.Key == previousEdge.Value))
.Equals(default(KeyValuePair<Vector3, Vector3>)))
{
// Our graph is not oriented but we want to ignores edges
// that go back from where we went
if (currentEdge.GetHashCode() == previousEdge.GetHashCode())
{
copy.Remove(currentEdge);
continue;
}
// Add the vertex to the list and continue
_coordsList.Add(currentEdge.Value);
previousEdge = currentEdge;
// Remove traversed nodes
copy.Remove(currentEdge);
}
}
A case might be made about the performance, has my algorithm has first to computes the edges, makes uses of the Vector3.Distance function and frequently uses a Contains() method.
Building the edges iterating over everyone of them using the distance is clearly not optimal performance wise, but I wanted to keep this part simple and I don't think the impact will make it that much different compared to the other algorithm proposed.
As for the Contains() method, keep in mind that it comes from a HashSet, making it of the same complexity as using the indexer operator on a Dictionnary or a List.
For the sake of completeness and ease of use, here's the full class :
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace DrawOuterGraph
{
public class OuterGraph: MonoBehaviour
{
private List<List<Vector3>> _squares;
private HashSet<KeyValuePair<Vector3, Vector3>> _edges;
private List<Vector3> _coordsList;
[SerializeField]
private float edgeDistance = 1;
private void Start()
{
CreateCoords();
ComputeEdges();
BuildList();
}
private void ComputeEdges()
{
// The edges collection is a set of pair of vertex
_edges = new HashSet<KeyValuePair<Vector3, Vector3>>();
foreach (var square in _squares)
{
// Iterate over the coordinates to compute the edges
// Using for loop to skip already processed edges
var squareCount = square.Count;
for (var i = 0; i < squareCount; i++)
{
// The source vertex
var src = square[i];
for (var j = 0; j < squareCount; j++)
{
if (i == j) continue;
// The vertex with whom we want to determine if they form and edge
var dest = square[j];
// Check the distance between them to filter out the diagonal edges
if (!(Math.Abs(Vector3.Distance(src, dest) - edgeDistance) < 0.001)) continue;
var edge = new KeyValuePair<Vector3, Vector3>(src, dest);
// _edges is a set, making it viable to use Contains
// even when the collections contains a lot of elements
if (_edges.Contains(edge))
{
// If the edge already exists in the set,
// it means its not part of the border
_edges.Remove(edge);
}
else
{
_edges.Add(edge);
}
}
}
}
}
public void BuildList()
{
_coordsList = new List<Vector3>();
// Make a copy of the edges so we can remove items from it
// without destroying the original collection
var copy = new HashSet<KeyValuePair<Vector3, Vector3>>(_edges);
// Add the first pair before starting the loop
var previousEdge = _edges.First();
_coordsList.Add(previousEdge.Key);
_coordsList.Add(previousEdge.Value);
KeyValuePair<Vector3, Vector3> currentEdge;
// While there is an edge that follows the previous one
while (!(currentEdge = copy.FirstOrDefault(pair => pair.Key == previousEdge.Value))
.Equals(default(KeyValuePair<Vector3, Vector3>)))
{
// Our graph is not oriented but we want to ignores edges
// that go back from where we went
if (currentEdge.GetHashCode() == previousEdge.GetHashCode())
{
copy.Remove(currentEdge);
continue;
}
// Add the vertex to the list and continue
_coordsList.Add(currentEdge.Value);
previousEdge = currentEdge;
// Remove traversed nodes
copy.Remove(currentEdge);
}
}
public void CreateCoords()
{
_squares = new List<List<Vector3>>()
{
new List<Vector3>()
{
new Vector3(0,0,1),
new Vector3(1,0,0),
new Vector3(0,0,0),
new Vector3(1,0,1),
},
new List<Vector3>()
{
new Vector3(0,0,1),
new Vector3(1,0,1),
new Vector3(1,0,2),
new Vector3(0,0,2),
},
new List<Vector3>()
{
new Vector3(1,0,1),
new Vector3(2,0,1),
new Vector3(2,0,2),
new Vector3(1,0,2),
},
new List<Vector3>()
{
new Vector3(2,0,1),
new Vector3(2,0,2),
new Vector3(3,0,1),
new Vector3(3,0,2),
},
new List<Vector3>()
{
new Vector3(2,0,1),
new Vector3(3,0,1),
new Vector3(2,0,0),
new Vector3(3,0,0),
},
new List<Vector3>()
{
new Vector3(4,0,1),
new Vector3(3,0,1),
new Vector3(4,0,0),
new Vector3(3,0,0),
},
new List<Vector3>()
{
new Vector3(1,0,2),
new Vector3(1,0,3),
new Vector3(2,0,2),
new Vector3(2,0,3),
},
new List<Vector3>()
{
new Vector3(3,0,2),
new Vector3(3,0,3),
new Vector3(2,0,2),
new Vector3(2,0,3),
},
new List<Vector3>()
{
new Vector3(3,0,4),
new Vector3(3,0,3),
new Vector3(2,0,4),
new Vector3(2,0,3),
},
new List<Vector3>()
{
new Vector3(1,0,4),
new Vector3(1,0,3),
new Vector3(2,0,4),
new Vector3(2,0,3),
},
new List<Vector3>()
{
new Vector3(3,0,4),
new Vector3(3,0,5),
new Vector3(2,0,4),
new Vector3(2,0,5),
},
new List<Vector3>()
{
new Vector3(3,0,2),
new Vector3(3,0,3),
new Vector3(4,0,2),
new Vector3(4,0,3),
},
};
}
private void OnDrawGizmos()
{
// Draw using the edges
if (_edges is not null)
{
foreach (var (src, dest) in _edges)
{
Gizmos.DrawLine(src, dest);
}
}
// Draw using the ordered list
if (_coordsList is null || _coordsList.Count == 0) return;
var previous = _coordsList[0];
foreach (var current in _coordsList)
{
Gizmos.DrawLine(previous, current);
previous = current;
}
}
}
}

I've found a way to help you.
I've written a function for you that does the following: iterates all the squares through their RectTransforms (easily replaceable with the Transform component), and for each of the squares it checks if its position is equal to another square with the side added up of the square. In this way, squares with at least one side in common are found.
If it finds a square with one side in common, it adds the two vertices in common to a list.
If at the end 4 vertices are found, the code understands that it is inside the figure and therefore must not have edges, but if three or less are found, they are added to the final list of external vertices.
The problem with the code is that it can be more optimized you have to customize the if () to check if they have a common side. In particular, since they are floats, it is simply not necessary to match them. I tried to round them up, but in particular cases there may be too great a margin of error. You can just change it by knowing the side of the square.
List<RectTransform> pos = new List<RectTransform>();
List<Vector2> results = new List<Vector2>();
int size = pos[0].sizeDelta.x;
for (int i = 0; i < pos.Count; i++)
{
List<Vector2> v = new List<Vector2>();
for (int o = 0; o < pos.Count; o++)
{
if (Mathf.Round(pos[i].position.x) == Mathf.Round(pos[o].position.x + size))
{
Add(new Vector2(pos[o].position.x + size / 2, pos[o].position.y + size / 2));
Add(new Vector2(pos[o].position.x + size / 2, pos[o].position.y - size / 2));
}
else if (Mathf.Round(pos[i].position.x) == Mathf.Round(pos[o].position.x - size))
{
Add(new Vector2(pos[o].position.x - size / 2, pos[o].position.y + size / 2));
Add(new Vector2(pos[o].position.x - size / 2, pos[o].position.y - size / 2));
}
else if (Mathf.Round(pos[i].position.y) == Mathf.Round(pos[o].position.y + size))
{
Add(new Vector2(pos[o].position.x + size / 2, pos[o].position.y + size / 2));
Add(new Vector2(pos[o].position.x - size / 2, pos[o].position.y + size / 2));
}
else if (Mathf.Round(pos[i].position.y) == Mathf.Round(pos[o].position.y - size))
{
Add(new Vector2(pos[o].position.x + size / 2, pos[o].position.y - size / 2));
Add(new Vector2(pos[o].position.x - size / 2, pos[o].position.y - size / 2));
}
if (v.Count == 4)
break;
}
if (v.Count == 4)
continue;
for (int o = 0; i < v.Count; o++)
if (!results.Contains(v[o]))
results.Add(v[o]);
void Add(Vector2 _v)
{
if (!v.Contains(_v))
v.Add(_v);
}
}
To create the line renderer that joins all these vertices, I suggest you think like this:
Choose a vertex to start from. Compare that vertex with all the
others and check if the distance between that vertex and the compared
vertex is equal to the side of the square. In this case it means that
it is above, below, to the right or to the left of the first vertex.
You will have a maximum of 4 results, and add them all to a list.
Now take a vertex you just found and use it to compare it to all the
others, doing the same thing as before. Also this time you will find
at most 4 vertices, with the distance from that vetice equal to the
side of the square. The difference is that for sure among those
vertices you will also find the first vertex analyzed, and then check
if it is already present, and if necessary remove it and add the one
found. They will have the same value, but the order will be
different.
Choose another vertex among those exited and start over with the for
() loop.
You have to be careful because there are some things you have to think about for it to work that I didn't specify because it would become very long.
As mentioned, if you are good with C # you will be able to transform this reasoning into code.
Good work!

I’m going to assume that the movement area is connected. If not, you can
do a flood fill and run this algorithm on each of the connected areas.
The first order of business is to find one of the clockwise arcs on the
outer boundary. The left edge of the leftmost square, oriented up, will
do. Starting with this arc, we trace clockwise around the boundary.
There are three cases for each movement, demonstrated in the Python code
below. When the boundary is straight, we suppress the interior points.
image = """
##
#
##
#### #
#####
##
#
"""
squares = {
(x + 0.5, y - 0.5)
for (y, line) in enumerate(image.splitlines())
for (x, c) in enumerate(line)
if c == "#"
}
print("squares =", squares)
leftmost = min(squares)
up = (0, -1)
x, y = leftmost
dx, dy = up
boundary = []
while True:
point = (x + 0.5 * (dx + dy), y + 0.5 * (dy - dx))
left = (x + dx + dy, y + dy - dx)
straight = (x + dx, y + dy)
if left in squares: # turn left
boundary.append(point)
x, y = left
dx, dy = dy, -dx
elif straight in squares: # go straight
x, y = straight
else: # turn right
boundary.append(point)
dx, dy = -dy, dx
if ((x, y), (dx, dy)) == (leftmost, up):
break
print("boundary =", boundary)
Output:
squares = {(3.5, 1.5), (5.5, 4.5), (0.5, 3.5), (2.5, 0.5), (1.5, 4.5), (3.5, 0.5), (2.5, 3.5), (3.5, 6.5), (3.5, 3.5), (2.5, 2.5), (5.5, 3.5), (4.5, 4.5), (3.5, 2.5), (2.5, 5.5), (1.5, 3.5), (3.5, 5.5), (2.5, 4.5), (3.5, 4.5)}
boundary = [(0.0, 3.0), (2.0, 3.0), (2.0, 2.0), (3.0, 2.0), (3.0, 1.0), (2.0, 1.0), (2.0, 0.0), (4.0, 0.0), (4.0, 4.0), (5.0, 4.0), (5.0, 3.0), (6.0, 3.0), (6.0, 5.0), (4.0, 5.0), (4.0, 7.0), (3.0, 7.0), (3.0, 6.0), (2.0, 6.0), (2.0, 5.0), (1.0, 5.0), (1.0, 4.0), (0.0, 4.0)]

My approach is as simple as you expect it to be, we just check for the neighborhood and create a line to represent the free side.
Just to contextualize, I'm using an array of bool to represent what is a valid square and what isn't, and a List<(Vector3 from, Vector3 to)> to represent the border, and with some Gizmos' help we ended with something like
private List<(Vector3 from, Vector3 to)> border = new List<(Vector3 from, Vector3 to)>();
public bool[,] ValidPositions = new bool[10, 10];
(the beautiful serialized matrix come from Odin)
As I said, simple as it can be, check for neighbor
private bool CheckBox (int i, int j, Side sideToCheck)
=> sideToCheck switch
{
Side.Up => j == 9 || !ValidPositions[i, j + 1], // By 9 I mean max range of lines in the array
Side.Right => i == 9 || !ValidPositions[i + 1, j],
Side.Down => j == 0 || !ValidPositions[i, j - 1],
Side.Left => i == 0 || !ValidPositions[i - 1, j],
_ => false
};
And add a line
private (Vector3 from, Vector3 to) AddLine (int i, int j, Side side)
=> side switch
{
Side.Up => (new Vector3(i, j + 1),
new Vector3(i + 1, j + 1)),
Side.Right => (new Vector3(i + 1, j),
new Vector3(i + 1, j + 1)),
Side.Down => (new Vector3(i, j),
new Vector3(i + 1, j)),
Side.Left => (new Vector3(i, j),
new Vector3(i, j + 1)),
};
Call it is just some conditionals
private void RefreshContourPosition()
{
border.Clear();
for (var i = 0; i < ValidPositions.GetLength(0); i++)
{
for (var j = 0; j < ValidPositions.GetLength(1); j++)
{
if (!ValidPositions[i, j])
continue;
if (CheckBox(i, j, Side.Up))
border.Add(AddLine(i, j, Side.Up));
if (CheckBox(i, j, Side.Right))
border.Add(AddLine(i, j, Side.Right));
if (CheckBox(i, j, Side.Down))
border.Add(AddLine(i, j, Side.Down));
if (CheckBox(i, j, Side.Left))
border.Add(AddLine(i, j, Side.Left));
}
}
}
After this refresh, your border will have all lines that is part of the desired border.
Hope this helps, N

This answer is a bit borderline Stackoverflow policy violation. :)
I understand that you ask for a specific solution, but I feel that as your question is still on a fairly theoretical design level I just wanted to drop some alternatives. I'm no pro, but my gut feeling is that line renderer will get quite heavy, especially for something like a basic grid.
If you are planning to have an all time visible base grid, I would probably take a look at already existing grid shaders to draw this. This is cheaper and the grid can be huge with no impact. It's also a lot easier to play around with when it comes to aesthetics, like adding glow or blend the lines against the underlying surface etc.
For a 2d game, the Unity TileMap is a must to checkout.
https://github.com/ogxd/grid-shader-unity
https://assetstore.unity.com/packages/tools/simple-grid-shader-119988
https://blog.devgenius.io/introduction-to-tilemap-in-unity-part-1-beb7b5435c2
When it comes to the outline. You might want to explore using tiles rather than drawing the edges. I think the tiles will be a lot easier to work with, both when it comes to game logic and making it look good. With a tile grid you could do bitmasking to draw nice edges. This will be resource cheap and opens up for endless possibilities to get that outline look really cool (like the curved glowing style xcom uses).
If it is a 2d game you can use the Unity tile map, and it might even be possible to use Unity auto tile for the outline.
If 3d each tile could just be a 3d mesh with texture. In 3d it would probably be better to generate a (procedural) mesh and outline it with an outline shader.
The short story is that I personally think using shaders/sprites for these kinds of things might be a better approach. :)
https://gamedevelopment.tutsplus.com/tutorials/how-to-use-tile-bitmasking-to-auto-tile-your-level-layouts--cms-25673
https://learn.unity.com/tutorial/using-rule-tiles#5fe9914fedbc2a28d93ce461
https://alexanderameye.github.io/notes/rendering-outlines/
Best of luck!

Related

Weird results with topojson.merge

I am trying to draw realistic (non-straight) borders of randomly generated countries using d3.geoVoronoi and topojson.merge. This is what I am doing:
I create 3 Voronoi diagrams, say A, B and C, respectively with 100, 1000, and 10000 cells.
I merge the voronoi cells of B (1000 cells) if their centroid is contained in the same cell of A (100 cells). In this way I obtain B', with 100 cells having irregular borders.
I repeat point 2, this time merging the cells of C (10000) if their centroid belongs to B'. I thus obtain B'', with 100 cells with even more realistic borders.
In principle the procedure could be repeated to obtain even more detailed borders, but I am already running into problems. When I draw B'' (black lines in the figures), it looks weird.
Why? Does it have to do with the winding order of B'? How can I fix this?
Figures:
Figure 1, Figure 2, Figure 3
const v_land = d3.geoVoronoi(land_Points);
let countries_polygons_land = v_land.polygons(land_Points);
const countries_topology2 = {
type: "Topology",
polygons: countries_polygons_land
};
const countries_topoJSON_land = topojson.topology(countries_topology2);
const countries_geojson_land = topojson.feature(countries_topoJSON_land, countries_topoJSON_land.objects.polygons);
// MERGE
let mergedGeoJSON = countries_geojson_land;
let featureCluster = create2DArray(mergedGeoJSON.features.length);
let newFeature;
for (let k=1; k<points.length; k++) {
// Merge smaller states within greater boundaries
for (let i=0; i < mergedGeoJSON.features.length; i++) {
for (let j=0; j < countries_geojson[k].features.length; j++) {
if(d3.geoContains(mergedGeoJSON.features[i], d3.geoCentroid(countries_geojson[k].features[j]))) {
featureCluster[i].push(countries_topoJSON[k].objects.polygons.geometries[j]);
}
}
}
// Create geoJSON
mergedGeoJSON = {
type: "FeatureCollection",
features: []
}
for (let i=0; i < featureCluster.length; i++) {
newFeature = {
type: "Feature",
geometry: topojson.merge(countries_topoJSON[k], featureCluster[i])
}
mergedGeoJSON.features.push(newFeature);
}
// Draw Polygons (realistic)
ctx.beginPath(),
path(mergedGeoJSON),
ctx.strokeStyle = `rgba(0, 0, 0, 1)`,
ctx.lineWidth = 0.5*k,
ctx.lineJoin = 'round',
ctx.stroke();
}

How to create an object depending on the position of the player along the Y axis?

The game is an endless runner, in the game the player starts to go down the slope and it new obstacles should now appear lower in the coordinate, but I still have no idea how to do this.
Instantiate(obj, new Vector3(20.46f, transform.position.y, 0), Quaternion.identity);
hi you can use transform.UP vector to get the upward direction to your player. use it like this:
newpos=transform.position+(transfom.up*5); 5 is upward space to player.
You should create a Vector 3 variable for your next position of terrain/tiles.
private Vector3 _nextTerrainPos;
In your update method/ or LateUpdate method you can check if the position of your player is greater than the terrain and then you can generate more terrain like I have done below:
if (Player.position.y > _nextTerrainPos.y)
{
_nextTerrainPos.y += 1000;
a= Random.Range(0, 10);
_nextTerrainPos.x = 0;
GenerateNextTerrain2D(new Vector2(_nextTerrainPos.x, _nextTerrainPos.y));
}
This is how you can instantiate your level and terrain:
GenerateNextTerrain2D(Vector terrainPos)
newTerrainObj = Instantiate(your level, _nextTerrainPos, Quaternion.identity) as GameObject;
newTerrainObj.SetActive(true);
//Camera.main.transform.position = new Vector3(117.4f,252.1f);
//Camera.main.orthographicSize = 46f;
DistanceHandler(newTerrainObj);
Here you can define the distance that has been covered:
public void DistanceHandler(GameObject tileset)
{
if (VehicleSimpleControl.DistanceCovered >= 500)
{
tileset.transform.GetChild(0).gameObject.SetActive(true);
}
if (VehicleSimpleControl.DistanceCovered >= 700)
{
tileset.transform.GetChild(1).gameObject.SetActive(true);
}
if (VehicleSimpleControl.DistanceCovered >= 900)
{
tileset.transform.GetChild(2).gameObject.SetActive(true);
}
if (VehicleSimpleControl.DistanceCovered >= 1100)
{
tileset.transform.GetChild(3).gameObject.SetActive(true);
}
}

2d circle rect collision and reflection doesnt work

I have game with map built by rectangles, darker rectangles (named "closed") mean its place where balls should be able to move, ball should reflect from the lighter rectangles(named "open") border. In future I'll add more balls and they will reflect from each other.
The problem is with new Vector after collision.
I force function circleRectGetCollisionNormal() to return vector(-1,0) what i think its normal for this case (ball is moving in right direction).
Ball is starting with degrees and change it simply to vector, this reflection worked for 45 degrees but when I change angle to 10 degrees ball moved into lighter rectangles(named "open").
Here is how it looks like (Picture)
I'm doing like this:
1-check if ball collided with lighter rectangle,
2-if it collided, I want to change direction so I return vector, for example for right side of ball colliding with rectangle return [-1,0] (I think its normal of vertical line, and its pointing left direction).
3-calculate new ball move Vector from this equation: newMoveVector = oldMoveVector − (2 * dotProduct(oldMoveVector, normalVector) * normalVector)
Here is code for each step:
1.
circleRect(circlePos, circleSize, rectPos, rectSize) {
//its rectRect collision but it doesnt matter because reflection surface is always horizontal or vertical
let r1 = {
left: circlePos.x - circleSize.x/2,
right: circlePos.x + circleSize.x/2,
top: circlePos.y - circleSize.y/2,
bottom: circlePos.y + circleSize.y/2
};
let r2 = {
left: rectPos.x,
right: rectPos.x + rectSize.x,
top: rectPos.y,
bottom: rectPos.y + rectSize.y
};
return !(r2.left > r1.right ||
r2.right < r1.left ||
r2.top > r1.bottom ||
r2.bottom < r1.top);
}
isOnOpenTile(pos: Vector, size: Vector) {
let openTiles = this.getTiles('open');
let result = false;
openTiles.forEach(element => {
if( this.circleRect(pos,size,element.pos,element.size) ){
result = element;
return;
}
});
return result;
}
2.
circleRectGetCollisionNormal(c, r) {
if(c.pos.y <= r.pos.y - (r.size.y/2)) return new Vector(0,-1);
//Hit was from below the brick
if(c.pos.y >= r.pos.y + (r.size.y/2)) return new Vector(0,1);
//Hit was from above the brick
if(c.pos.x < r.pos.x) return new Vector(1,0);
//Hit was on left
if(c.pos.x > r.pos.x) return new Vector(-1,0);
//Hit was on right
return false;
}
3.
getNewMoveVector(moveVector, normalVector) {
normalVector = this.normalize(normalVector);
let dot = (moveVector.x * moveVector.y) + (normalVector.x * normalVector.y);
let dotProduct = new Vector(dot, dot);
return moveVector.sub(dotProduct.mult(normalVector).mult(new Vector(2,2)));
}
normalize(v) {
let length = Math.sqrt((v.x*v.x) + (v.y*v.y));
return new Vector(v.x/length,v.y/length);
}
And here is main function for this
getMoveVectorOnCollision(circle) {
let coll = this.isOnOpenTile( circle.pos, circle.size );
if( coll != false) {
let vector = this.circleRectGetCollisionNormal(circle, coll);
return this.getNewMoveVector(circle.moveVector, vector);
} else return false;
}
Object Vector always contain 2 values all of function (mult, sub, div, add) work like here.
sub(vector: Vector) {
return new Vector(this.x - vector.x, this.y - vector.y);
}
Please give me advice, actual solution or tell about different way to do this reflection. I wasted more than 3 days trying to solve this, I have to move on.
Yor dot product calculation is erroneous. Change these lines:
let dot = (moveVector.x * moveVector.y) + (normalVector.x * normalVector.y);
let dotProduct = new Vector(dot, dot);
by this one line:
let dotProduct = (moveVector.x * normalVector.x + moveVector.y * normalVector.y);
Note that dotProduct is scalar value, not vector, so you have to make vector for subtraction as
subvec.x = 2 * dotProduct * normalVector.x
subvec.y = 2 * dotProduct * normalVector.y
and
return moveVector.sub(subvec);

SceneKit: orient one node toward another in 3Dspace

I need to orient one node to point its Z-axis at another node in 3D. Yeah, the perfect job for the LookAtConstraint. And for most of my work LookAt is fine. But when I apply LookAt to a particular node, I can no longer animate that node's translation with SCNAction. Picture a hydrogen atom leaving a molecule as it ionizes. The orientation is needed to properly rotate the bond (a cylinder) bewteen the hydrogen and an oxygen atom on the molecule.
I can orient the bond FROM the oxygen TO the hydrogen and animate. But this disorients most of the other bonds which were getting by just fine with LookAt's.
I gave this a mighty try before realizing it answers a somewhat different question:
Calculate rotations to look at a 3D point?
I had a similar issue with a project. What I eventually realized was that I need to use multiple constraints. One for translation (movement) and the other using the look at constraint.
I would move the object and then apply the look at constraint; in this case, it was a camera following an objects being moved using actions. Code snippet follows:
let targetNodeConstraint = SCNLookAtConstraint(target: someObject)
targetNodeConstraint.gimbalLockEnabled = true
let followObjectConstraint = SCNTransformConstraint(inWorldSpace: true, withBlock: { (node, matrix) -> SCNMatrix4 in
let transformMatrix = SCNMatrix4MakeTranslation(
self.someObject.position.x - 1.0,
self.someObject.position.y, self.someObject.position.z + 1.0)
return transformMatrix
})
// Position the object behind the other object & rotate it to
roadCamera.constraints = [followObjectConstraint, targetNodeConstraint]
The important thing to note is the order in which the constraints are added to the object using an array. In the code above, I am ignoring the current matrix before I apply a transform matrix (I should re-write this code someday)
The complete source code of this "experiment" is on GitHub as I try things out.
https://github.com/ManjitBedi/CubeTrip
Hopefully, this is helpful.
My solution here. Deal with situation that node continuously translate in space and should always toward a position.
#discardableResult
func yew(_ node:SCNNode, toPosition position:SCNVector3) -> Float
{
var eularAngle = SCNVector3Zero
let tranform = node.transform
var forward = GLKVector3Make(tranform.m31, tranform.m32, tranform.m33)
var toWard = GLKVector3Make(position.x - node.position.x, position.y - node.position.y, position.z - node.position.z)
forward = GLKVector3Normalize(GLKVector3Make(forward.x, 0, forward.z))
toWard = GLKVector3Normalize(GLKVector3Make(toWard.x, 0, toWard.z))
var dotProduct = GLKVector3DotProduct(forward,toWard)
dotProduct = (dotProduct > 1) ? 1 : ((dotProduct < -1) ? -1 : dotProduct)
var yew = acos(dotProduct)
if yew < 0 {
assert(false)
}
//toward is clockwise of forward
let isCW = GLKVector3CrossProduct(forward, toWard).y < 0
if isCW {
yew = -yew
}
eularAngle.y = yew
node.eulerAngles = SCNVector3Make(eularAngle.x + wrapperNode.eulerAngles.x,
eularAngle.y + wrapperNode.eulerAngles.y,
eularAngle.z + wrapperNode.eulerAngles.z)
return yew
}
#discardableResult
func pitch(_ node:SCNNode, toPosition position:SCNVector3) -> Float{
var eularAngle = SCNVector3Zero
let tranform = node.transform
var toWard = GLKVector3Make(position.x - node.position.x, position.y - node.position.y, position.z - node.position.z)
var forward = GLKVector3Make(tranform.m31, tranform.m32, tranform.m33)
forward = GLKVector3Normalize(forward)
toWard = GLKVector3Normalize(toWard)
var dotProduct = GLKVector3DotProduct(forward,toWard)
dotProduct = (dotProduct > 1) ? 1 : ((dotProduct < -1) ? -1 : dotProduct)
var pitch = acos(dotProduct)
//toward is clockwise of forward, if right vector of model and crossProfuct.x has same direction
let crossProduct = GLKVector3CrossProduct(forward, toWard)
let isCW = (crossProduct.x <= 0) != (tranform.m11 <= 0)
if isCW {
pitch = -pitch
}
eularAngle.x = pitch
node.eulerAngles = SCNVector3Make(eularAngle.x + node.eulerAngles.x,
eularAngle.y + node.eulerAngles.y,
eularAngle.z + node.eulerAngles.z)
return pitch
}
func orient(_ node:SCNNode, toPosition position:SCNVector3) {
self.yew(node, toPosition: position)
self.pitch(node, toPosition: position)
}

An algorithm to space out overlapping rectangles?

This problem actually deals with roll-overs, I'll just generalized below as such:
I have a 2D view, and I have a number of rectangles within an area on the screen. How do I spread out those boxes such that they don't overlap each other, but only adjust them with minimal moving?
The rectangles' positions are dynamic and dependent on user's input, so their positions could be anywhere.
Attached images show the problem and desired solution
The real life problem deals with rollovers, actually.
Answers to the questions in the comments
Size of rectangles is not fixed, and is dependent on the length of the text in the rollover
About screen size, right now I think it's better to assume that the size of the screen is enough for the rectangles. If there is too many rectangles and the algo produces no solution, then I just have to tweak the content.
The requirement to 'move minimally' is more for asethetics than an absolute engineering requirement. One could space out two rectangles by adding a vast distance between them, but it won't look good as part of the GUI. The idea is to get the rollover/rectangle as close as to its source (which I will then connect to the source with a black line). So either 'moving just one for x' or 'moving both for half x' is fine.
I was working a bit in this, as I also needed something similar, but I had delayed the algorithm development. You helped me to get some impulse :D
I also needed the source code, so here it is. I worked it out in Mathematica, but as I haven't used heavily the functional features, I guess it'll be easy to translate to any procedural language.
A historic perspective
First I decided to develop the algorithm for circles, because the intersection is easier to calculate. It just depends on the centers and radii.
I was able to use the Mathematica equation solver, and it performed nicely.
Just look:
It was easy. I just loaded the solver with the following problem:
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
]
As straightforward as that, and Mathematica did all the work.
I said "Ha! it's easy, now let's go for the rectangles!". But I was wrong ...
Rectangular Blues
The main problem with the rectangles is that querying the intersection is a nasty function. Something like:
So, when I tried to feed up Mathematica with a lot of these conditions for the equation, it performed so badly that I decided to do something procedural.
My algorithm ended up as follows:
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
You may note that the "smallest movement" condition is not completely satisfied (only in one direction). But I found that moving the rectangles in any direction to satisfy it, sometimes ends up with a confusing map changing for the user.
As I am designing a user interface, I choose to move the rectangle a little further, but in a more predictable way. You can change the algorithm to inspect all angles and all radii surrounding its current position until an empty place is found, although it'll be much more demanding.
Anyway, these are examples of the results (before/ after):
Edit> More examples here
As you may see, the "minimum movement" is not satisfied, but the results are good enough.
I'll post the code here because I'm having some trouble with my SVN repository. I'll remove it when the problems are solved.
Edit:
You may also use R-Trees for finding rectangle intersections, but it seems an overkill for dealing with a small number of rectangles. And I haven't the algorithms already implemented. Perhaps someone else can point you to an existing implementation on your platform of choice.
Warning! Code is a first approach .. not great quality yet, and surely has some bugs.
It's 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 *)];)
];
Main
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!
Edit: Multi-angle searching
I implemented a change in the algorithm allowing to search in all directions, but giving preference to the axis imposed by the geometric symmetry.
At the expense of more cycles, this resulted in more compact final configurations, as you can see here below:
More samples here.
The pseudocode for the main loop changed to:
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
I'm not including the source code for brevity, but just ask for it if you think you can use it. I think that, should you go this way, it's better to switch to R-trees (a lot of interval tests needed here)
Here's a guess.
Find the center C of the bounding box of your rectangles.
For each rectangle R that overlaps another.
Define a movement vector v.
Find all the rectangles R' that overlap R.
Add a vector to v proportional to the vector between the center of R and R'.
Add a vector to v proportional to the vector between C and the center of R.
Move R by v.
Repeat until nothing overlaps.
This incrementally moves the rectangles away from each other and the center of all the rectangles. This will terminate because the component of v from step 4 will eventually spread them out enough all by itself.
I think this solution is quite similar to the one given by cape1232, but it's already implemented, so worth checking out :)
Follow to this reddit discussion: http://www.reddit.com/r/gamedev/comments/1dlwc4/procedural_dungeon_generation_algorithm_explained/ and check out the description and implementation. There's no source code available, so here's my approach to this problem in AS3 (works exactly the same, but keeps rectangles snapped to grid's resolution):
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;
}
}
}
}
I really like b005t3r's implementation! It works in my test cases, however my rep is too low to leave a comment with the 2 suggested fixes.
You should not be translating rooms by single resolution increments, you should translate by the velocity you just pain stakingly calculated! This makes the separation more organic as deeply intersected rooms separate more each iteration than not-so-deeply intersecting rooms.
You should not assume velociites less than 0.5 means rooms are separate as you can get stuck in a case where you are never separated. Imagine 2 rooms intersect, but are unable to correct themselves because whenever either one attempts to correct the penetration they calculate the required velocity as < 0.5 so they iterate endlessly.
Here is a Java solution (: 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);
Here's an algorithm written using Java for handling a cluster of unrotated Rectangles. It allows you to specify the desired aspect ratio of the layout and positions the cluster using a parameterised Rectangle as an anchor point, which all translations made are oriented about. You can also specify an arbitrary amount of padding which you'd like to spread the Rectangles by.
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());
}
}
}
}
Here's an example using an AspectRatio of 1.2, a FillPercentage of 0.8 and a Padding of 10.0.
This is a deterministic approach which allows spacing to occur around the anchor whilst leaving the location of the anchor itself unchanged. This allows the layout to occur around wherever the user's Point of Interest is. The logic for selecting a position is pretty simplistic, but I think the surrounding architecture of sorting the elements based upon their initial position and then iterating them is a useful approach for implementing a relatively predictable distribution. Plus we're not relying on iterative intersection tests or anything like that, just building up some bounding boxes to give us a broad indication of where to align things; after this, applying padding just comes kind of naturally.
Here is a version that takes cape1232's answer and is a standalone runnable example for 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();
}
});
}
}

Resources