Final Edit
I figured out the issue and posted a solution as an answer below. If you have stumbled here from google looking for a decent way to move QGraphicsItems / QGraphicsObjects via the ItemIsMovable
flag while avoiding collision with other nodes, I included a working itemChange
method at the very end of my answer.
My original project dealt with snapping nodes to an arbitrary grid, but that is easily removed and not required at all for this solution to work. Efficiency wise, it's not the most clever use of data structures or algorithms, so if you end up with a lot of nodes on the screen you may want to try something more intelligent.
Original Question
I have a subclassed QGraphicsItem, Node, that sets the flags for ItemIsMovable
and ItemSendsGeometryChanges
. These Node objects have overriden shape()
and boundingRect()
functions that give them a basic rectangle to move around.
Moving a node around with the mouse calls the itemChange()
function as expected, and I can call a snapPoint()
function that forces the moved Node to always snap to a grid. (This is a simple function that returns a new QPointF based on a given param, where the new point is restricted to the closest coordinate on the actual snapping grid). This works perfectly as it is now.
My big issue is that I'd like to avoid snapping into a position that collides with other Nodes. That is, if I move a Node into a resulting (snapped to grid) location, I'd like the collidingItems()
list to be completely empty.
I can accurately detect the collidingItems
after the ItemPositionHasChanged
flag gets thrown in itemChange()
, but unfortunately this is after the Node is already moved. More to the point, I found this collision list isn't properly updated until the Node returns from the initial itemChange()
(which is bad since the Node has already been moved into an invalid location that overlaps some other nodes).
Here's the itemChange()
function I've got so far:
QVariant Node::itemChange(GraphicsItemChange change, const QVariant &value)
{
if ( change == ItemPositionChange && scene() )
{
// Snap the value to the grid
QPointF pt = value.toPointF();
QPointF snapped = snapPoint(pt);
// How much we've moved by
qreal delX = snapped.x() - pos().x();
qreal delY = snapped.y() - pos().y();
// We didn't move enough to justify a new snapped position
// Visually, this results in no changes and no collision testing
// is required, so we can quit early
if ( delX == 0 && delY == 0 )
return snapped;
// If we're here, then we know the Node's position has actually
// updated, and we know exactly how far it's moved based on delX
// and delY (which is constrained to the snap grid dimensions)
// for example: delX may be -16 and delY may be 0 if we've moved
// left on a grid of 16px size squares
// Ideally, this is the location where I'd like to check for
// collisions before returning the snapped point. If I can detect
// if there are any nodes colliding with this one now, I can avoid
// moving it at all and avoid the ItemPositionHasChanged checks
// below
return snapped;
}
else if ( change == ItemPositionHasChanged && scene())
{
// This gets fired after the Node actually gets moved (once return
// snapped gets called above)
// The collidingItems list is now validly set and can be compared
// but unfortunately its too late, as we've already moved into a
// potentially offending position that overlaps other Nodes
if ( collidingItems().empty() )
{
// This is okay
}
else
{
// This is bad! This is what we wanted to avoid
}
}
return QGraphicsItem::itemChange(change, value);
}
What I've tried
I tried storing the delX
and delY
changes, and if we entered the final else block above, I tried to reverse the previous move with moveBy(-lastDelX, -lastDelY)
. This had some horrible results, since this gets called while the mouse is still pressed and moving the Node itself (which ends up moving the Node many times while attempting to move around a collision, often resulting in the Node flying off the side of the screen very quickly).
I also tried storing previous (known valid) points, and calling a setX and setY on the position to revert back if a collision was found, but this also had similar issues to the above approach.
Conclusion
If I could find a way to detect the collidingItems()
accurately before the move even happens, I'd be able to avoid the ItemPositionHasChanged
block altogether, making sure that the ItemPositionChange
only returns points that are valid, and thus avoiding any logic for moving to a previously valid position.
Thanks for your time, and please let me know if you need more code or examples to illustrate the issue better.
Edit 1:
I think I've stumbled onto a technique that will probably form the basis for the actual solution, but I still need some help since it's not entirely working.
In the itemChange()
function before the last return snapped;
, I added the following code:
// Make a copy of the shape and translate it correctly
// This should be equivalent to the shape formed after a successful
// move (i.e. after return snapped;)
QPainterPath path = shape();
path.translate(delX, delY);
// Examine all the other items to see if they collide with the desired
// shape....
for (QGraphicsItem* other : canvas->items() )
{
if ( other == this ) // don't care about this node, obviously
continue;
if ( other->collidesWithPath(path) )
{
// We should hopefully only reach this if another
// item collides with the desired path. If that happens,
// we don't want to move this node at all. Unfortunately,
// we always seem to enter this block even when the nodes
// shouldn't actually collide.
qDebug() << "Found collision?";
return pos();
}
}
// Should only reach this if no collisions found, so we're okay to move
return snapped;
...but this doesn't work either. Any new node always enters the collidesWithPath
block, even though they are nowhere near each other. The canvas object in the above code is just the QGraphicsView that holds all of these Node objects, in case that's unclear too.
Any ideas how to make the collidesWithPath
work correctly? Is the path that copied the shape()
wrong? I'm not sure what's going wrong here, as the comparison of the two shapes seems to work fine after the move completes (i.e. I can detect collisions accurately afterwards). I can upload the entire code if that helps.